From 2e2217b5a9d6c95d4511af34e03057eb93b3b0b1 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 5 Mar 2026 19:26:22 +0100 Subject: [PATCH 01/83] Adding single value constructor to Stock --- src/main/java/millions/Stock.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/millions/Stock.java b/src/main/java/millions/Stock.java index 886ecba..ba59d70 100644 --- a/src/main/java/millions/Stock.java +++ b/src/main/java/millions/Stock.java @@ -14,6 +14,12 @@ public Stock(String symbol, String company, List prices) { this.prices = prices; } + public Stock(String symbol, String company, BigDecimal initialPrice) { + this.symbol = symbol; + this.company = company; + this.prices = List.of(initialPrice); + } + public String getSymbol() { return this.symbol; } From 2d2244d8410740512de3b0f2e9ab9aa1609ca5c4 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 10:10:51 +0100 Subject: [PATCH 02/83] adding null check and int quantity overloader --- src/main/java/millions/Exchange.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/millions/Exchange.java b/src/main/java/millions/Exchange.java index 98584f2..372d44b 100644 --- a/src/main/java/millions/Exchange.java +++ b/src/main/java/millions/Exchange.java @@ -18,6 +18,10 @@ public Exchange(String name, List stockList) { this.stocks = new HashMap<>(); this.weekNumber = 1; + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Exchange name cannot be null or blank"); + } + // Populate the stocks map to get ticker -> stock for (Stock stock : stockList) { this.stocks.put(stock.getSymbol(), stock); @@ -30,6 +34,10 @@ public void buy(Player player, Stock stock, BigDecimal quantity) { purchase.commit(player); } + public void buy(Player player, Stock stock, int quantity) { + this.buy(player, stock, BigDecimal.valueOf(quantity)); + } + public void sell(Player player, Share share) { Sale sale = new Sale(share, weekNumber); sale.commit(player); From 60d38720f218e402632ff90e122e45062ec04434 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 12:22:18 +0100 Subject: [PATCH 03/83] Changing call order, adding null checks --- src/main/java/millions/Exchange.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/millions/Exchange.java b/src/main/java/millions/Exchange.java index 372d44b..57d077c 100644 --- a/src/main/java/millions/Exchange.java +++ b/src/main/java/millions/Exchange.java @@ -28,17 +28,23 @@ public Exchange(String name, List stockList) { } } - public void buy(Player player, Stock stock, BigDecimal quantity) { + public void buy(String symbol, Player player, BigDecimal quantity) { + + Stock stock = this.stocks.get(symbol); + if (stock == null) { + throw new IllegalArgumentException("Stock not found"); + } + Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); Purchase purchase = new Purchase(shareToBuy, this.weekNumber); purchase.commit(player); } - public void buy(Player player, Stock stock, int quantity) { - this.buy(player, stock, BigDecimal.valueOf(quantity)); + public void buy(String symbol, Player player, int quantity) { + this.buy(symbol, player, BigDecimal.valueOf(quantity)); } - public void sell(Player player, Share share) { + public void sell(Share share, Player player) { Sale sale = new Sale(share, weekNumber); sale.commit(player); } From 945cc6c1b4685389d4191001fc0db3a8e2733add Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 12:22:38 +0100 Subject: [PATCH 04/83] Changing stocks from List to ArrayList --- src/main/java/millions/Stock.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/Stock.java b/src/main/java/millions/Stock.java index ba59d70..41a3ead 100644 --- a/src/main/java/millions/Stock.java +++ b/src/main/java/millions/Stock.java @@ -1,6 +1,7 @@ package millions; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; public class Stock { @@ -11,13 +12,13 @@ public class Stock { public Stock(String symbol, String company, List prices) { this.symbol = symbol; this.company = company; - this.prices = prices; + this.prices = new ArrayList<>(prices); } public Stock(String symbol, String company, BigDecimal initialPrice) { this.symbol = symbol; this.company = company; - this.prices = List.of(initialPrice); + this.prices = new ArrayList<>(List.of(initialPrice)); } public String getSymbol() { From 292c14b42d10f1641a4f1f3ab3fd0292212bf706 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 12:23:17 +0100 Subject: [PATCH 05/83] Adding salesPrice and quantity update to calculator --- src/main/java/millions/calculators/SaleCalculator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/millions/calculators/SaleCalculator.java b/src/main/java/millions/calculators/SaleCalculator.java index 2bddf30..1ceaed6 100644 --- a/src/main/java/millions/calculators/SaleCalculator.java +++ b/src/main/java/millions/calculators/SaleCalculator.java @@ -12,6 +12,8 @@ public class SaleCalculator implements TransactionCalculator { public SaleCalculator(Share share) { super(); this.purchasePrice = share.getPurchasePrice(); + this.salesPrice = share.getStock().getSalesPrice(); + this.quantity = share.getQuantity(); } @Override From 6b33b970c9a6ebb6ca0f2e1d2c2ee75bdc843381 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 12:24:07 +0100 Subject: [PATCH 06/83] test: adding ExchangeTest --- src/test/java/millions/ExchangeTest.java | 80 +++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index a852a0d..f0479b2 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -2,4 +2,82 @@ import static org.junit.jupiter.api.Assertions.*; -class ExchangeTest {} +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ExchangeTest { + @Test + public void testGetters() { + Stock s1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s2 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + Stock s3 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2)); + + assertFalse(exchange.hasStock("MSFT")); + assertTrue(exchange.getStock("DOGL").equals(s2)); + assertNull(exchange.getStock("XYZ")); + assertTrue(exchange.findStocks("Amozon").isEmpty()); + + assertTrue(exchange.hasStock("DOGL")); + assertFalse(exchange.findStocks("Pear").isEmpty()); + assertFalse(exchange.findStocks("PE").isEmpty()); + + assertTrue(exchange.findStocks("Inc").size() == 2); + } + + @Test + public void happyPath() { + Stock s1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s2 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + Stock s3 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2, s3)); + Player player = new Player("name", BigDecimal.valueOf(1000)); + + BigDecimal previousMoney = player.getMoney(); + exchange.buy("PEAR", player, 2); + assertTrue(previousMoney.compareTo(player.getMoney()) > 0); + + BigDecimal previousPrice = s1.getSalesPrice(); + exchange.advance(); + assertFalse(previousPrice.equals(s1.getSalesPrice())); + + Share share = player.getPortfolio().getShares().getFirst(); + + previousMoney = player.getMoney(); + exchange.sell(share, player); + assertTrue(previousMoney.compareTo(player.getMoney()) < 0); + assertTrue(player.getPortfolio().getShares().isEmpty()); + } + + @Test + public void testNullsAndInvalid() { + Stock s1 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + + Exchange exchange = new Exchange("exchange", List.of(s1)); + Player player = new Player("name", BigDecimal.valueOf(1000)); + + Share unownedShare = new Share(s1, BigDecimal.valueOf(1), s1.getSalesPrice()); + assertThrows(IllegalStateException.class, () -> exchange.sell(unownedShare, player)); + + Player noMoney = new Player("nomoney", BigDecimal.valueOf(0)); + assertThrows(IllegalStateException.class, () -> exchange.buy("MSFT", noMoney, 1)); + + assertThrows( + IllegalArgumentException.class, + () -> new Exchange("", List.of(new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300))))); + + assertThrows( + IllegalArgumentException.class, + () -> new Exchange(null, List.of(new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300))))); + + assertThrows(IllegalArgumentException.class, () -> exchange.buy("DOGL", player, 2)); + + assertThrows(IllegalArgumentException.class, () -> exchange.buy("DOGL", player, -2)); + + assertThrows( + IllegalArgumentException.class, () -> exchange.buy("DOGL", player, BigDecimal.valueOf(-2))); + } +} From f7e1db69f88ad4de9f43c0bd337d00e124966281 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:14:55 +0100 Subject: [PATCH 07/83] Player adding null and negative checks --- src/main/java/millions/Player.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/millions/Player.java b/src/main/java/millions/Player.java index 747c8d0..6c01c2c 100644 --- a/src/main/java/millions/Player.java +++ b/src/main/java/millions/Player.java @@ -15,13 +15,27 @@ public Player(String name, BigDecimal startingMoney) { this.money = startingMoney; this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); + + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Player name cannot be null or blank"); + } + + if (startingMoney == null || startingMoney.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Starting money cannot be null or negative"); + } } public void addMoney(BigDecimal amount) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount cannot be null or negative"); + } this.money = this.money.add(amount); } public void withdrawMoney(BigDecimal amount) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount cannot be null or negative"); + } this.money = this.money.subtract(amount); } From a898b7f722bd727d214a810d405aa42d8bba03c0 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:15:19 +0100 Subject: [PATCH 08/83] Adding int quantity overload for Share --- src/main/java/millions/Share.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/millions/Share.java b/src/main/java/millions/Share.java index 6eddada..afb3947 100644 --- a/src/main/java/millions/Share.java +++ b/src/main/java/millions/Share.java @@ -13,6 +13,10 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; } + public Share(Stock stock, int quantity, BigDecimal purchasePrice) { + this(stock, BigDecimal.valueOf(quantity), purchasePrice); + } + public Stock getStock() { return this.stock; } From e81d68bcadcc848b92a73cd89572152e4cc2d0a6 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:16:17 +0100 Subject: [PATCH 09/83] test: adding playerTest --- src/test/java/millions/PlayerTest.java | 35 +++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/PlayerTest.java index 62a3263..99cfb45 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/PlayerTest.java @@ -2,4 +2,37 @@ import static org.junit.jupiter.api.Assertions.*; -class PlayerTest {} +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class PlayerTest { + + @Test + public void happyPath() { + Player player = new Player("name", BigDecimal.valueOf(1000)); + assertEquals(BigDecimal.valueOf(1000), player.getMoney()); + + player.addMoney(BigDecimal.valueOf(500)); + assertEquals(BigDecimal.valueOf(1500), player.getMoney()); + + player.withdrawMoney(BigDecimal.valueOf(200)); + assertEquals(BigDecimal.valueOf(1300), player.getMoney()); + } + + @Test + public void testGetters() { + Player player = new Player("name", BigDecimal.valueOf(1000)); + assertEquals("name", player.getName()); + assertEquals(BigDecimal.valueOf(1000), player.getMoney()); + assertTrue(player.getPortfolio().getShares().isEmpty()); + assertTrue(player.getTransactionArchive().isEmpty()); + } + + @Test + public void testNullsAndInvalid() { + assertThrows(IllegalArgumentException.class, () -> new Player(null, BigDecimal.valueOf(1000))); + assertThrows(IllegalArgumentException.class, () -> new Player("", BigDecimal.valueOf(1000))); + assertThrows(IllegalArgumentException.class, () -> new Player("name", null)); + assertThrows(IllegalArgumentException.class, () -> new Player("name", BigDecimal.valueOf(-1))); + } +} From 13001ef77e3f52514eed61af2cf2770cd03332ff Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:16:46 +0100 Subject: [PATCH 10/83] test: adding PortfolioTest --- src/test/java/millions/PortfolioTest.java | 61 ++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/PortfolioTest.java b/src/test/java/millions/PortfolioTest.java index 3fb1019..69bb678 100644 --- a/src/test/java/millions/PortfolioTest.java +++ b/src/test/java/millions/PortfolioTest.java @@ -2,4 +2,63 @@ import static org.junit.jupiter.api.Assertions.*; -class PortfolioTest {} +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class PortfolioTest { + + @Test + public void happyPath() { + Portfolio portfolio = new Portfolio(); + Stock stock = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Share share = new Share(stock, 10, BigDecimal.valueOf(300)); + + assertTrue(portfolio.addShare(share)); + assertTrue(portfolio.contains(share)); + assertEquals(1, portfolio.getShares().size()); + assertEquals(share, portfolio.getShares().get(0)); + + assertTrue(portfolio.removeShare(share)); + assertFalse(portfolio.contains(share)); + assertTrue(portfolio.getShares().isEmpty()); + } + + @Test + public void testGettersAndSetters() { + Portfolio portfolio = new Portfolio(); + Stock stock1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock stock2 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + Share share1 = new Share(stock1, 10, BigDecimal.valueOf(300)); + Share share2 = new Share(stock2, 5, BigDecimal.valueOf(200)); + + portfolio.addShare(share1); + portfolio.addShare(share2); + + assertEquals(2, portfolio.getShares().size()); + assertTrue(portfolio.getShares().contains(share1)); + assertTrue(portfolio.getShares().contains(share2)); + + assertEquals(1, portfolio.getShares("PEAR").size()); + assertTrue(portfolio.getShares("PEAR").contains(share1)); + assertFalse(portfolio.getShares("PEAR").contains(share2)); + + assertEquals(1, portfolio.getShares("DOGL").size()); + assertFalse(portfolio.getShares("DOGL").contains(share1)); + assertTrue(portfolio.getShares("DOGL").contains(share2)); + + assertTrue(portfolio.getShares("XYZ").isEmpty()); + + portfolio.removeShare(share1); + assertEquals(1, portfolio.getShares().size()); + } + + @Test + public void testNullsAndInvalid() { + Stock stock1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Share share1 = new Share(stock1, 10, BigDecimal.valueOf(300)); + Portfolio portfolio = new Portfolio(); + assertFalse(portfolio.removeShare(null)); + assertFalse(portfolio.contains(null)); + assertFalse(portfolio.removeShare(share1)); + } +} From 4e90cef79148719e8bcd3e8f265d22a59f9ea4f8 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:54:39 +0100 Subject: [PATCH 11/83] Adding null checks to Share --- src/main/java/millions/Share.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/millions/Share.java b/src/main/java/millions/Share.java index afb3947..5db6738 100644 --- a/src/main/java/millions/Share.java +++ b/src/main/java/millions/Share.java @@ -11,6 +11,16 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.stock = stock; this.quantity = quantity; this.purchasePrice = purchasePrice; + + if (stock == null) { + throw new IllegalArgumentException("Stock cannot be null"); + } + if (quantity == null) { + throw new IllegalArgumentException("Quantity cannot be null"); + } + if (purchasePrice == null) { + throw new IllegalArgumentException("Purchase price cannot be null"); + } } public Share(Stock stock, int quantity, BigDecimal purchasePrice) { From eea0cf2b9822ec3330fa5c67d1fcf8cf0507b9a6 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:54:59 +0100 Subject: [PATCH 12/83] Adding null and blank checks to Stock --- src/main/java/millions/Stock.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/millions/Stock.java b/src/main/java/millions/Stock.java index 41a3ead..632c4b7 100644 --- a/src/main/java/millions/Stock.java +++ b/src/main/java/millions/Stock.java @@ -13,12 +13,18 @@ public Stock(String symbol, String company, List prices) { this.symbol = symbol; this.company = company; this.prices = new ArrayList<>(prices); + + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); + } + + if (company == null || company.isBlank()) { + throw new IllegalArgumentException("Company cannot be null or blank"); + } } public Stock(String symbol, String company, BigDecimal initialPrice) { - this.symbol = symbol; - this.company = company; - this.prices = new ArrayList<>(List.of(initialPrice)); + this(symbol, company, new ArrayList<>(List.of(initialPrice))); } public String getSymbol() { From ed67b31e9b372a4645d80cdf99f85903bcc114c5 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:55:37 +0100 Subject: [PATCH 13/83] test: Adding PurchaseTest --- src/test/java/millions/PurchaseTest.java | 37 +++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/PurchaseTest.java b/src/test/java/millions/PurchaseTest.java index a67c37a..4cbb6cc 100644 --- a/src/test/java/millions/PurchaseTest.java +++ b/src/test/java/millions/PurchaseTest.java @@ -2,4 +2,39 @@ import static org.junit.jupiter.api.Assertions.*; -class PurchaseTest {} +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class PurchaseTest { + @Test + public void testHappyPath() { + Stock stock = new Stock("TestStock", "TST", BigDecimal.valueOf(10)); + Share share = new Share(stock, 2, BigDecimal.valueOf(10)); + Player player = new Player("TestPlayer", BigDecimal.valueOf(100)); + Purchase purchase = new Purchase(share, 1); + purchase.commit(player); + assertTrue(purchase.isCommitted()); + + // 1 less because of tax + assertEquals(79, player.getMoney().intValue()); + assertTrue(player.getPortfolio().getShares().contains(share)); + } + + @Test + public void testNullsAndInvalid() { + + Stock stock = new Stock("TestStock", "TST", BigDecimal.valueOf(10)); + Share share = new Share(stock, 2, BigDecimal.valueOf(10)); + Player player = new Player("TestPlayer", BigDecimal.valueOf(100)); + Purchase purchase = new Purchase(share, 1); + + // Double commit + purchase.commit(player); + assertThrows(IllegalStateException.class, () -> purchase.commit(player)); + + // Insufficient funds + Player poorPlayer = new Player("PoorPlayer", BigDecimal.valueOf(10)); + Purchase expensivePurchase = new Purchase(new Share(stock, 20, BigDecimal.valueOf(10)), 1); + assertThrows(IllegalStateException.class, () -> expensivePurchase.commit(poorPlayer)); + } +} From 54bc98cefa569519befcbe1327c6cba345c7c5cd Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 13:55:58 +0100 Subject: [PATCH 14/83] test: Adding SaleTest --- src/test/java/millions/SaleTest.java | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/SaleTest.java b/src/test/java/millions/SaleTest.java index b808f7a..a9202b7 100644 --- a/src/test/java/millions/SaleTest.java +++ b/src/test/java/millions/SaleTest.java @@ -2,4 +2,31 @@ import static org.junit.jupiter.api.Assertions.*; -class SaleTest {} +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class SaleTest { + + @Test + public void testHappyPath() { + Stock stock = new Stock("TestStock", "TST", BigDecimal.valueOf(10)); + Share share = new Share(stock, 2, BigDecimal.valueOf(10)); + Player player = new Player("TestPlayer", BigDecimal.valueOf(100)); + player.getPortfolio().addShare(share); + Sale sale = new Sale(share, 1); + sale.commit(player); + assertTrue(sale.isCommitted()); + + assertEquals(120, player.getMoney().intValue()); + assertFalse(player.getPortfolio().getShares().contains(share)); + } + + @Test + public void testNullsAndInvalid() { + Stock stock = new Stock("TestStock", "TST", BigDecimal.valueOf(10)); + Share share = new Share(stock, 2, BigDecimal.valueOf(10)); + Player player = new Player("TestPlayer", BigDecimal.valueOf(100)); + Sale sale = new Sale(share, 1); + assertThrows(IllegalStateException.class, () -> sale.commit(player)); + } +} From 963481b493489465709fb825c756e45ed39de755 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 14:28:47 +0100 Subject: [PATCH 15/83] test: adding ShareTest --- src/test/java/millions/ShareTest.java | 31 ++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/ShareTest.java b/src/test/java/millions/ShareTest.java index cf3d676..0925c5b 100644 --- a/src/test/java/millions/ShareTest.java +++ b/src/test/java/millions/ShareTest.java @@ -2,4 +2,33 @@ import static org.junit.jupiter.api.Assertions.*; -class ShareTest {} +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class ShareTest { + + @Test + public void testHappyPath() { + Stock stock = new Stock("AYO", "Ayhoo", BigDecimal.valueOf(10)); + Share share = new Share(stock, 5, BigDecimal.valueOf(10)); + assertEquals(BigDecimal.valueOf(5), share.getQuantity()); + } + + @Test + public void testGetters() { + Stock stock = new Stock("AYO", "Ayhoo", BigDecimal.valueOf(10)); + Share share = new Share(stock, 5, BigDecimal.valueOf(10)); + assertEquals(BigDecimal.valueOf(5), share.getQuantity()); + assertEquals(stock, share.getStock()); + assertEquals(BigDecimal.valueOf(10), share.getPurchasePrice()); + } + + @Test + public void testNullsAndInvalid() { + Stock stock = new Stock("AYO", "Ayhoo", BigDecimal.valueOf(10)); + assertThrows(IllegalArgumentException.class, () -> new Share(null, 2, BigDecimal.valueOf(2))); + assertThrows(IllegalArgumentException.class, () -> new Share(stock, 2, null)); + assertThrows( + IllegalArgumentException.class, () -> new Share(stock, null, BigDecimal.valueOf(2))); + } +} From d090837b469b46c967f72df623956dcbf72b6111 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 14:29:05 +0100 Subject: [PATCH 16/83] test: adding StockTest --- src/test/java/millions/StockTest.java | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/StockTest.java b/src/test/java/millions/StockTest.java index eda8962..13a95a8 100644 --- a/src/test/java/millions/StockTest.java +++ b/src/test/java/millions/StockTest.java @@ -2,4 +2,39 @@ import static org.junit.jupiter.api.Assertions.*; -class StockTest {} +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class StockTest { + @Test + public void testHapyPath() { + Stock stock = new Stock("NVDA", "Nvadia", BigDecimal.valueOf(20)); + Stock stock2 = + new Stock( + "NVDA", "Nvadia", new ArrayList(Arrays.asList(BigDecimal.valueOf(20)))); + stock2.addNewSalesPrice(BigDecimal.valueOf(30)); + assertEquals(BigDecimal.valueOf(30), stock2.getSalesPrice()); + } + + @Test + public void settersAndGetters() { + Stock stock = new Stock("NVDA", "Nvadia", BigDecimal.valueOf(20)); + assertEquals("NVDA", stock.getSymbol()); + assertEquals("Nvadia", stock.getCompany()); + } + + @Test + public void testNullsAndInvalid() { + + assertThrows( + IllegalArgumentException.class, () -> new Stock(null, "Nvadia", BigDecimal.valueOf(20))); + assertThrows( + IllegalArgumentException.class, () -> new Stock("NVDA", null, BigDecimal.valueOf(20))); + assertThrows( + IllegalArgumentException.class, () -> new Stock("NVDA", "", BigDecimal.valueOf(20))); + assertThrows( + IllegalArgumentException.class, () -> new Stock("", "Nvadia", BigDecimal.valueOf(20))); + } +} From 992f8fcdf3b548ca3404370bb36e260ddc184887 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 14:29:33 +0100 Subject: [PATCH 17/83] test: adding TransactionArchiveTest --- .../java/millions/TransactionArchiveTest.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/test/java/millions/TransactionArchiveTest.java b/src/test/java/millions/TransactionArchiveTest.java index bdc9ca8..64d1c2c 100644 --- a/src/test/java/millions/TransactionArchiveTest.java +++ b/src/test/java/millions/TransactionArchiveTest.java @@ -2,4 +2,45 @@ import static org.junit.jupiter.api.Assertions.*; -class TransactionArchiveTest {} +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TransactionArchiveTest { + + TransactionArchive archive; + Purchase purchase; + Sale sale; + + @BeforeEach + public void setUp() { + archive = new TransactionArchive(); + Stock stock = new Stock("SUS", "Samsung", BigDecimal.valueOf(10)); + Share share = new Share(stock, 2, BigDecimal.valueOf(10)); + purchase = new Purchase(share, 1); + sale = new Sale(share, 2); + } + + @Test + public void testHappyPath() { + assertTrue(archive.isEmpty()); + archive.add(purchase); + assertFalse(archive.isEmpty()); + } + + @Test + public void testNullsAndInvalid() { + archive.add(purchase); + assertFalse(archive.add(purchase)); + } + + @Test + public void testGetters() { + archive.add(purchase); + archive.add(sale); + assertEquals(List.of(purchase), archive.getPurchases(1)); + assertEquals(List.of(sale), archive.getSales(2)); + assertEquals(2, archive.countDistinctWeeks()); + } +} From ccce043778f737b6e2cfcd537d24d81b96672aa0 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 14:30:12 +0100 Subject: [PATCH 18/83] removing test for abstract class --- src/test/java/millions/TransactionTest.java | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/test/java/millions/TransactionTest.java diff --git a/src/test/java/millions/TransactionTest.java b/src/test/java/millions/TransactionTest.java deleted file mode 100644 index 9074225..0000000 --- a/src/test/java/millions/TransactionTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package millions; - -import static org.junit.jupiter.api.Assertions.*; - -class TransactionTest {} From 55e2dfe8bf592f1bd256076c13eb17954042f02e Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 6 Mar 2026 14:38:55 +0100 Subject: [PATCH 19/83] Adding shell.nix --- pom.xml | 2 +- shell.nix | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 shell.nix diff --git a/pom.xml b/pom.xml index 5a5267a..704507d 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.junit.jupiter junit-jupiter - 6.0.1 + 5.11.0 test diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a9cc7b4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs ? import {} }: + +let + jdk = pkgs.jdk25; +in pkgs.mkShell { + buildInputs = [ jdk pkgs.maven ]; + JAVA_HOME = "${jdk}"; +} From 61c5e901230182082d948d7d362488f13d4854a4 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Tue, 10 Mar 2026 12:46:04 +0100 Subject: [PATCH 20/83] Changed commited field in transaction class as this was a mistake in the project description for part 1 --- src/main/java/millions/Transaction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/millions/Transaction.java b/src/main/java/millions/Transaction.java index b4b4022..22fc754 100644 --- a/src/main/java/millions/Transaction.java +++ b/src/main/java/millions/Transaction.java @@ -7,7 +7,7 @@ public abstract class Transaction { private Share share; private int week; private TransactionCalculator transactionCalculator; - private boolean committed; + protected boolean committed; protected Transaction(Share share, int week, TransactionCalculator transactionCalculator) { this.share = share; From 4efe591049a42d8bfb8f623329d5a8609962fc47 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Tue, 10 Mar 2026 13:07:00 +0100 Subject: [PATCH 21/83] Added methods to stock class: - getHistoricalPrices: returns a list of all prices for a stock - getHighestPrice: returns the highest historical price for a stock - getLowestPrice: returns the lowest historical price for a stock - getLatestPriceChange: returns the difference between the current and previous price for a stock --- src/main/java/millions/Stock.java | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/main/java/millions/Stock.java b/src/main/java/millions/Stock.java index 886ecba..c40f9ff 100644 --- a/src/main/java/millions/Stock.java +++ b/src/main/java/millions/Stock.java @@ -29,4 +29,35 @@ public BigDecimal getSalesPrice() { public void addNewSalesPrice(BigDecimal price) { this.prices.add(price); } + + public List getHistoricalPrices() { + return this.prices; + } + + public BigDecimal getHighestPrice() { + BigDecimal highestPrice = this.prices.get(0); + for (BigDecimal price : this.prices) { + if (price.compareTo(highestPrice) > 0) { + highestPrice = price; + } + } + return highestPrice; + } + + public BigDecimal getLowestPrice() { + BigDecimal lowestPrice = this.prices.get(0); + for (BigDecimal price : this.prices) { + if (price.compareTo(lowestPrice) < 0) { + lowestPrice = price; + } + } + return lowestPrice; + } + + public BigDecimal getLatestPriceChange() { + BigDecimal currentPrice = this.prices.getLast(); + BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); + + return currentPrice.subtract(lastPrice); + } } From 35bf19c5d90dad15cbb3369ca9a3f7644868b9c0 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 11 Mar 2026 16:28:41 +0100 Subject: [PATCH 22/83] Created StockInformationCSVReader --- .../millions/StockInformationCSVReader.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/millions/StockInformationCSVReader.java diff --git a/src/main/java/millions/StockInformationCSVReader.java b/src/main/java/millions/StockInformationCSVReader.java new file mode 100644 index 0000000..ea0fef9 --- /dev/null +++ b/src/main/java/millions/StockInformationCSVReader.java @@ -0,0 +1,38 @@ +package millions; + +import java.io.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + + +public class StockInformationCSVReader { + File file; + + public StockInformationCSVReader(File file) { + this.file = file; + } + + public List readFile() { + List stocks = new ArrayList<>(); + try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { + List lines = bufferedReader.readAllLines(); + for (String line : lines) { + // Skips comment and blank lines + if ( !(line.startsWith("#") || line.isEmpty()) ) { + String[] data = line.split(","); + // Ensures only fields of the correct length are read + if (data.length == 3) { + String symbol = data[0]; + String company = data[1]; + BigDecimal price = new BigDecimal(data[2]); + stocks.add(new Stock(symbol, company, price)); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return stocks; + } +} \ No newline at end of file From 072dbc1049ef04ee9ca87050305a633d197c6d57 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 11 Mar 2026 16:41:21 +0100 Subject: [PATCH 23/83] Created StockInformationCSVWriter --- .../millions/StockInformationCSVReader.java | 2 +- .../millions/StockInformationCSVWriter.java | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/main/java/millions/StockInformationCSVWriter.java diff --git a/src/main/java/millions/StockInformationCSVReader.java b/src/main/java/millions/StockInformationCSVReader.java index ea0fef9..7b36a63 100644 --- a/src/main/java/millions/StockInformationCSVReader.java +++ b/src/main/java/millions/StockInformationCSVReader.java @@ -7,7 +7,7 @@ public class StockInformationCSVReader { - File file; + private final File file; public StockInformationCSVReader(File file) { this.file = file; diff --git a/src/main/java/millions/StockInformationCSVWriter.java b/src/main/java/millions/StockInformationCSVWriter.java new file mode 100644 index 0000000..f36dd18 --- /dev/null +++ b/src/main/java/millions/StockInformationCSVWriter.java @@ -0,0 +1,34 @@ +package millions; + +import java.io.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class StockInformationCSVWriter { + private final List stocks; + private final File destinationFile; + + public StockInformationCSVWriter(List stocks,File destinationFile) { + this.stocks = stocks; + this.destinationFile = destinationFile; + } + + public void write() { + StringBuilder builder = new StringBuilder(); + for (Stock stock : stocks) { + builder.append(stock.getSymbol()); + builder.append(","); + builder.append(stock.getCompany()); + builder.append(","); + // Unsure if price history or just latest price should be saved + builder.append(stock.getSalesPrice().toPlainString()); + builder.append("\n"); + } + try (FileWriter writer = new FileWriter(destinationFile); BufferedWriter bufferedWriter = new BufferedWriter(writer)) { + bufferedWriter.write(builder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } +} From 0771753a73b567be8c296b6a2ed8df78a72598a9 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 8 Apr 2026 11:23:54 +0200 Subject: [PATCH 24/83] Added getNetWorth methods to player and portfolio --- src/main/java/millions/Player.java | 6 ++++++ src/main/java/millions/Portfolio.java | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/millions/Player.java b/src/main/java/millions/Player.java index 6c01c2c..e9f541a 100644 --- a/src/main/java/millions/Player.java +++ b/src/main/java/millions/Player.java @@ -39,6 +39,12 @@ public void withdrawMoney(BigDecimal amount) { this.money = this.money.subtract(amount); } + public BigDecimal getNetWorth() { + BigDecimal netWorth = this.money; + netWorth = netWorth.add(this.portfolio.getNetWorth()); + return netWorth; + } + public String getName() { return this.name; } diff --git a/src/main/java/millions/Portfolio.java b/src/main/java/millions/Portfolio.java index e6fe889..35447fb 100644 --- a/src/main/java/millions/Portfolio.java +++ b/src/main/java/millions/Portfolio.java @@ -1,5 +1,8 @@ package millions; +import millions.calculators.SaleCalculator; + +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -28,6 +31,14 @@ public List getShares(String symbol) { .toList(); } + public BigDecimal getNetWorth() { + BigDecimal netWorth = new BigDecimal(0); + for (Share share : shares) { + netWorth = netWorth.add(new SaleCalculator(share).calculateTotal()); + } + return netWorth; + } + public boolean contains(Share share) { return this.shares.contains(share); } From a1089196862783960ea6584cb7fc18095d7acfec Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 8 Apr 2026 13:19:02 +0200 Subject: [PATCH 25/83] Added status for player along with test --- src/main/java/millions/Player.java | 16 ++++++++++++++++ src/test/java/millions/PlayerTest.java | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/millions/Player.java b/src/main/java/millions/Player.java index e9f541a..c68f842 100644 --- a/src/main/java/millions/Player.java +++ b/src/main/java/millions/Player.java @@ -1,6 +1,7 @@ package millions; import java.math.BigDecimal; +import java.math.RoundingMode; public class Player { private String name; @@ -8,6 +9,8 @@ public class Player { private BigDecimal money; private Portfolio portfolio; private TransactionArchive transactionArchive; + //temporary attribute until a better solution is found + public int weeksTraded; public Player(String name, BigDecimal startingMoney) { this.name = name; @@ -45,6 +48,19 @@ public BigDecimal getNetWorth() { return netWorth; } + public String getStatus() { + String status = "Novice"; + BigDecimal netWorth = getNetWorth(); + BigDecimal netWorthChange = netWorth.divide(startingMoney, RoundingMode.DOWN); + if (netWorthChange.compareTo(BigDecimal.valueOf(0.20)) >= 0 && weeksTraded >= 10) { + status = "Investor"; + } + if (netWorthChange.compareTo(BigDecimal.valueOf(0.40)) >= 0 && weeksTraded >= 20) { + status = "Speculator"; + } + return status; + } + public String getName() { return this.name; } diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/PlayerTest.java index 99cfb45..dc3fa3d 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/PlayerTest.java @@ -35,4 +35,22 @@ public void testNullsAndInvalid() { assertThrows(IllegalArgumentException.class, () -> new Player("name", null)); assertThrows(IllegalArgumentException.class, () -> new Player("name", BigDecimal.valueOf(-1))); } + + @Test + public void testStatus() { + Player player = new Player("name", BigDecimal.valueOf(1000)); + assertEquals("Novice", player.getStatus()); + + player.addMoney(BigDecimal.valueOf(200)); + assertEquals("Novice", player.getStatus()); + + player.weeksTraded = 10; + assertEquals("Investor", player.getStatus()); + + player.addMoney(BigDecimal.valueOf(200)); + assertEquals("Investor", player.getStatus()); + + player.weeksTraded = 20; + assertEquals("Speculator", player.getStatus()); + } } From 043369c5aa0cbe30ff1c6a441aca069c08ac029d Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 8 Apr 2026 15:56:54 +0200 Subject: [PATCH 26/83] Created 3 files: - TransactionCalculatorFactory - PurchaseCalculatorFactory - SaleCalculatorFactory --- .../calculators/PurchaseCalculatorFactory.java | 18 ++++++++++++++++++ .../calculators/SaleCalculatorFactory.java | 16 ++++++++++++++++ .../TransactionCalculatorFactory.java | 8 ++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/millions/calculators/PurchaseCalculatorFactory.java create mode 100644 src/main/java/millions/calculators/SaleCalculatorFactory.java create mode 100644 src/main/java/millions/calculators/TransactionCalculatorFactory.java diff --git a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java new file mode 100644 index 0000000..7f496cf --- /dev/null +++ b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java @@ -0,0 +1,18 @@ +package millions.calculators; + +import millions.Share; + +public class PurchaseCalculatorFactory extends TransactionCalculatorFactory { + Share share; + + @Override + public void setShare(Share share) { + this.share = share; + } + + @Override + public TransactionCalculator createCalculator() { + return new PurchaseCalculator(share); + } + +} diff --git a/src/main/java/millions/calculators/SaleCalculatorFactory.java b/src/main/java/millions/calculators/SaleCalculatorFactory.java new file mode 100644 index 0000000..fd16818 --- /dev/null +++ b/src/main/java/millions/calculators/SaleCalculatorFactory.java @@ -0,0 +1,16 @@ +package millions.calculators; + +import millions.Share; + +public class SaleCalculatorFactory extends TransactionCalculatorFactory { + Share share; + + @Override + public void setShare(Share share) { + this.share = share; + } + @Override + public TransactionCalculator createCalculator() { + return new SaleCalculator(share); + } +} diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java new file mode 100644 index 0000000..106ca73 --- /dev/null +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -0,0 +1,8 @@ +package millions.calculators; + +import millions.Share; + +public abstract class TransactionCalculatorFactory { + public abstract void setShare(Share share); + public abstract TransactionCalculator createCalculator(); +} From c5332bef06cda3ad66d67023279709bcc0f5d0a9 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 14:01:08 +0200 Subject: [PATCH 27/83] Refactored package structure to follow the MVC-pattern --- .../StockInformationReader.java} | 8 +++++--- .../StockInformationWriter.java} | 10 +++++----- src/main/java/millions/{ => model}/Exchange.java | 2 +- src/main/java/millions/{ => model}/Player.java | 2 +- src/main/java/millions/{ => model}/Portfolio.java | 4 ++-- src/main/java/millions/{ => model}/Purchase.java | 4 ++-- src/main/java/millions/{ => model}/Sale.java | 4 ++-- src/main/java/millions/{ => model}/Share.java | 2 +- src/main/java/millions/{ => model}/Stock.java | 2 +- src/main/java/millions/{ => model}/Transaction.java | 4 ++-- .../java/millions/{ => model}/TransactionArchive.java | 2 +- .../{ => model}/calculators/PurchaseCalculator.java | 4 ++-- .../{ => model}/calculators/SaleCalculator.java | 4 ++-- .../{ => model}/calculators/TransactionCalculator.java | 2 +- src/test/java/millions/ExchangeTest.java | 5 +++++ src/test/java/millions/PlayerTest.java | 2 ++ src/test/java/millions/PortfolioTest.java | 4 ++++ src/test/java/millions/PurchaseTest.java | 5 +++++ src/test/java/millions/SaleTest.java | 5 +++++ src/test/java/millions/ShareTest.java | 3 +++ src/test/java/millions/StockTest.java | 2 ++ src/test/java/millions/TransactionArchiveTest.java | 2 ++ 22 files changed, 56 insertions(+), 26 deletions(-) rename src/main/java/millions/{StockInformationCSVReader.java => controller/StockInformationReader.java} (87%) rename src/main/java/millions/{StockInformationCSVWriter.java => controller/StockInformationWriter.java} (81%) rename src/main/java/millions/{ => model}/Exchange.java (98%) rename src/main/java/millions/{ => model}/Player.java (98%) rename src/main/java/millions/{ => model}/Portfolio.java (92%) rename src/main/java/millions/{ => model}/Purchase.java (88%) rename src/main/java/millions/{ => model}/Sale.java (88%) rename src/main/java/millions/{ => model}/Share.java (97%) rename src/main/java/millions/{ => model}/Stock.java (98%) rename src/main/java/millions/{ => model}/Transaction.java (90%) rename src/main/java/millions/{ => model}/TransactionArchive.java (98%) rename src/main/java/millions/{ => model}/calculators/PurchaseCalculator.java (92%) rename src/main/java/millions/{ => model}/calculators/SaleCalculator.java (94%) rename src/main/java/millions/{ => model}/calculators/TransactionCalculator.java (86%) diff --git a/src/main/java/millions/StockInformationCSVReader.java b/src/main/java/millions/controller/StockInformationReader.java similarity index 87% rename from src/main/java/millions/StockInformationCSVReader.java rename to src/main/java/millions/controller/StockInformationReader.java index 7b36a63..0e98210 100644 --- a/src/main/java/millions/StockInformationCSVReader.java +++ b/src/main/java/millions/controller/StockInformationReader.java @@ -1,4 +1,6 @@ -package millions; +package millions.controller; + +import millions.model.Stock; import java.io.*; import java.math.BigDecimal; @@ -6,10 +8,10 @@ import java.util.List; -public class StockInformationCSVReader { +public class StockInformationReader { private final File file; - public StockInformationCSVReader(File file) { + public StockInformationReader(File file) { this.file = file; } diff --git a/src/main/java/millions/StockInformationCSVWriter.java b/src/main/java/millions/controller/StockInformationWriter.java similarity index 81% rename from src/main/java/millions/StockInformationCSVWriter.java rename to src/main/java/millions/controller/StockInformationWriter.java index f36dd18..692bc75 100644 --- a/src/main/java/millions/StockInformationCSVWriter.java +++ b/src/main/java/millions/controller/StockInformationWriter.java @@ -1,15 +1,15 @@ -package millions; +package millions.controller; + +import millions.model.Stock; import java.io.*; -import java.math.BigDecimal; -import java.util.ArrayList; import java.util.List; -public class StockInformationCSVWriter { +public class StockInformationWriter { private final List stocks; private final File destinationFile; - public StockInformationCSVWriter(List stocks,File destinationFile) { + public StockInformationWriter(List stocks, File destinationFile) { this.stocks = stocks; this.destinationFile = destinationFile; } diff --git a/src/main/java/millions/Exchange.java b/src/main/java/millions/model/Exchange.java similarity index 98% rename from src/main/java/millions/Exchange.java rename to src/main/java/millions/model/Exchange.java index 57d077c..fd4d7c1 100644 --- a/src/main/java/millions/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/src/main/java/millions/Player.java b/src/main/java/millions/model/Player.java similarity index 98% rename from src/main/java/millions/Player.java rename to src/main/java/millions/model/Player.java index c68f842..9b8ce1f 100644 --- a/src/main/java/millions/Player.java +++ b/src/main/java/millions/model/Player.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/src/main/java/millions/Portfolio.java b/src/main/java/millions/model/Portfolio.java similarity index 92% rename from src/main/java/millions/Portfolio.java rename to src/main/java/millions/model/Portfolio.java index 35447fb..f5f99d6 100644 --- a/src/main/java/millions/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -1,6 +1,6 @@ -package millions; +package millions.model; -import millions.calculators.SaleCalculator; +import millions.model.calculators.SaleCalculator; import java.math.BigDecimal; import java.util.ArrayList; diff --git a/src/main/java/millions/Purchase.java b/src/main/java/millions/model/Purchase.java similarity index 88% rename from src/main/java/millions/Purchase.java rename to src/main/java/millions/model/Purchase.java index 72f02ad..70bf608 100644 --- a/src/main/java/millions/Purchase.java +++ b/src/main/java/millions/model/Purchase.java @@ -1,6 +1,6 @@ -package millions; +package millions.model; -import millions.calculators.PurchaseCalculator; +import millions.model.calculators.PurchaseCalculator; public class Purchase extends Transaction { diff --git a/src/main/java/millions/Sale.java b/src/main/java/millions/model/Sale.java similarity index 88% rename from src/main/java/millions/Sale.java rename to src/main/java/millions/model/Sale.java index 25f2919..79ef8b0 100644 --- a/src/main/java/millions/Sale.java +++ b/src/main/java/millions/model/Sale.java @@ -1,6 +1,6 @@ -package millions; +package millions.model; -import millions.calculators.SaleCalculator; +import millions.model.calculators.SaleCalculator; public class Sale extends Transaction { diff --git a/src/main/java/millions/Share.java b/src/main/java/millions/model/Share.java similarity index 97% rename from src/main/java/millions/Share.java rename to src/main/java/millions/model/Share.java index 5db6738..615dc68 100644 --- a/src/main/java/millions/Share.java +++ b/src/main/java/millions/model/Share.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import java.math.BigDecimal; diff --git a/src/main/java/millions/Stock.java b/src/main/java/millions/model/Stock.java similarity index 98% rename from src/main/java/millions/Stock.java rename to src/main/java/millions/model/Stock.java index 9de5641..feff8bb 100644 --- a/src/main/java/millions/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import java.math.BigDecimal; import java.util.ArrayList; diff --git a/src/main/java/millions/Transaction.java b/src/main/java/millions/model/Transaction.java similarity index 90% rename from src/main/java/millions/Transaction.java rename to src/main/java/millions/model/Transaction.java index 22fc754..8dfdd4f 100644 --- a/src/main/java/millions/Transaction.java +++ b/src/main/java/millions/model/Transaction.java @@ -1,6 +1,6 @@ -package millions; +package millions.model; -import millions.calculators.TransactionCalculator; +import millions.model.calculators.TransactionCalculator; public abstract class Transaction { diff --git a/src/main/java/millions/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java similarity index 98% rename from src/main/java/millions/TransactionArchive.java rename to src/main/java/millions/model/TransactionArchive.java index 0a83e92..5e8b407 100644 --- a/src/main/java/millions/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/millions/calculators/PurchaseCalculator.java b/src/main/java/millions/model/calculators/PurchaseCalculator.java similarity index 92% rename from src/main/java/millions/calculators/PurchaseCalculator.java rename to src/main/java/millions/model/calculators/PurchaseCalculator.java index e23293c..1f7b341 100644 --- a/src/main/java/millions/calculators/PurchaseCalculator.java +++ b/src/main/java/millions/model/calculators/PurchaseCalculator.java @@ -1,7 +1,7 @@ -package millions.calculators; +package millions.model.calculators; import java.math.BigDecimal; -import millions.Share; +import millions.model.Share; public class PurchaseCalculator implements TransactionCalculator { BigDecimal purchasePrice; diff --git a/src/main/java/millions/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java similarity index 94% rename from src/main/java/millions/calculators/SaleCalculator.java rename to src/main/java/millions/model/calculators/SaleCalculator.java index 1ceaed6..96bc6b1 100644 --- a/src/main/java/millions/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -1,8 +1,8 @@ -package millions.calculators; +package millions.model.calculators; import java.math.BigDecimal; import java.math.RoundingMode; -import millions.Share; +import millions.model.Share; public class SaleCalculator implements TransactionCalculator { BigDecimal purchasePrice; diff --git a/src/main/java/millions/calculators/TransactionCalculator.java b/src/main/java/millions/model/calculators/TransactionCalculator.java similarity index 86% rename from src/main/java/millions/calculators/TransactionCalculator.java rename to src/main/java/millions/model/calculators/TransactionCalculator.java index 8b85c6a..d2d3917 100644 --- a/src/main/java/millions/calculators/TransactionCalculator.java +++ b/src/main/java/millions/model/calculators/TransactionCalculator.java @@ -1,4 +1,4 @@ -package millions.calculators; +package millions.model.calculators; import java.math.BigDecimal; diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index f0479b2..ca4c2fc 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -4,6 +4,11 @@ import java.math.BigDecimal; import java.util.List; + +import millions.model.Exchange; +import millions.model.Player; +import millions.model.Share; +import millions.model.Stock; import org.junit.jupiter.api.Test; class ExchangeTest { diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/PlayerTest.java index dc3fa3d..839b52c 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/PlayerTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; + +import millions.model.Player; import org.junit.jupiter.api.Test; class PlayerTest { diff --git a/src/test/java/millions/PortfolioTest.java b/src/test/java/millions/PortfolioTest.java index 69bb678..286da55 100644 --- a/src/test/java/millions/PortfolioTest.java +++ b/src/test/java/millions/PortfolioTest.java @@ -3,6 +3,10 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; + +import millions.model.Portfolio; +import millions.model.Share; +import millions.model.Stock; import org.junit.jupiter.api.Test; class PortfolioTest { diff --git a/src/test/java/millions/PurchaseTest.java b/src/test/java/millions/PurchaseTest.java index 4cbb6cc..941ac87 100644 --- a/src/test/java/millions/PurchaseTest.java +++ b/src/test/java/millions/PurchaseTest.java @@ -3,6 +3,11 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; + +import millions.model.Player; +import millions.model.Purchase; +import millions.model.Share; +import millions.model.Stock; import org.junit.jupiter.api.Test; class PurchaseTest { diff --git a/src/test/java/millions/SaleTest.java b/src/test/java/millions/SaleTest.java index a9202b7..1c02005 100644 --- a/src/test/java/millions/SaleTest.java +++ b/src/test/java/millions/SaleTest.java @@ -3,6 +3,11 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; + +import millions.model.Player; +import millions.model.Sale; +import millions.model.Share; +import millions.model.Stock; import org.junit.jupiter.api.Test; class SaleTest { diff --git a/src/test/java/millions/ShareTest.java b/src/test/java/millions/ShareTest.java index 0925c5b..25862ee 100644 --- a/src/test/java/millions/ShareTest.java +++ b/src/test/java/millions/ShareTest.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; + +import millions.model.Share; +import millions.model.Stock; import org.junit.jupiter.api.Test; class ShareTest { diff --git a/src/test/java/millions/StockTest.java b/src/test/java/millions/StockTest.java index 13a95a8..50afb44 100644 --- a/src/test/java/millions/StockTest.java +++ b/src/test/java/millions/StockTest.java @@ -5,6 +5,8 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; + +import millions.model.Stock; import org.junit.jupiter.api.Test; class StockTest { diff --git a/src/test/java/millions/TransactionArchiveTest.java b/src/test/java/millions/TransactionArchiveTest.java index 64d1c2c..c87ef93 100644 --- a/src/test/java/millions/TransactionArchiveTest.java +++ b/src/test/java/millions/TransactionArchiveTest.java @@ -4,6 +4,8 @@ import java.math.BigDecimal; import java.util.List; + +import millions.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From e2713196aa50c69646f7a6cf733e54d1da6b498b Mon Sep 17 00:00:00 2001 From: Nikolai Oliver Aasheim Lydvo Date: Sun, 12 Apr 2026 14:22:05 +0200 Subject: [PATCH 28/83] Delete .vscode directory --- .vscode/settings.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ed34fc0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "interactive", - "java.format.settings.profile": "GoogleStyle", - "editor.formatOnSave": true, - "editor.defaultFormatter": "redhat.java", -} From 84c1a03dc57374409de9fd10b4c0bbfaf0d7e3a2 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 15:06:28 +0200 Subject: [PATCH 29/83] Moved parsing of stock information to csvStockFileParser and added format verification to allow for other file formats in the future --- .../controller/CSVStockFileParser.java | 40 +++++++++++++++++++ .../controller/StockInformationReader.java | 21 ++-------- 2 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 src/main/java/millions/controller/CSVStockFileParser.java diff --git a/src/main/java/millions/controller/CSVStockFileParser.java b/src/main/java/millions/controller/CSVStockFileParser.java new file mode 100644 index 0000000..fc39da8 --- /dev/null +++ b/src/main/java/millions/controller/CSVStockFileParser.java @@ -0,0 +1,40 @@ +package millions.controller; + +import millions.model.Stock; + +import javax.swing.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class CSVStockFileParser { + private List lines; + + public CSVStockFileParser(List lines) { + if (verifyCSV(lines)) { + this.lines = lines; + } + else { + // throw file format error + } + } + + // returns true if all entries have exactly 3 data points + private static boolean verifyCSV(List lines) { + return lines.stream() + .filter(l -> !(l.startsWith("#") || l.isEmpty())) + .anyMatch(l -> l.split(",").length != 3); + } + + public List parse() { + List stocks = new ArrayList<>(); + lines.forEach(l -> { + String[] split = l.split(","); + String symbol = split[0]; + String company = split[1]; + BigDecimal price = new BigDecimal(split[2]); + stocks.add(new Stock(symbol, company, price)); + }); + return stocks; + } +} diff --git a/src/main/java/millions/controller/StockInformationReader.java b/src/main/java/millions/controller/StockInformationReader.java index 0e98210..c942461 100644 --- a/src/main/java/millions/controller/StockInformationReader.java +++ b/src/main/java/millions/controller/StockInformationReader.java @@ -15,26 +15,13 @@ public StockInformationReader(File file) { this.file = file; } - public List readFile() { - List stocks = new ArrayList<>(); + public List readFile() { + List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { - List lines = bufferedReader.readAllLines(); - for (String line : lines) { - // Skips comment and blank lines - if ( !(line.startsWith("#") || line.isEmpty()) ) { - String[] data = line.split(","); - // Ensures only fields of the correct length are read - if (data.length == 3) { - String symbol = data[0]; - String company = data[1]; - BigDecimal price = new BigDecimal(data[2]); - stocks.add(new Stock(symbol, company, price)); - } - } - } + lines = bufferedReader.readAllLines(); } catch (IOException e) { e.printStackTrace(); } - return stocks; + return lines; } } \ No newline at end of file From ea0abe1fae724516736b4ac5aeb7edb12afa0c8f Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 15:33:51 +0200 Subject: [PATCH 30/83] Created StockFileWriter interface and moved csv write to its own file to open up for additional formats in the future --- .../controller/CSVStockFileWriter.java | 35 +++++++++++++++++++ .../millions/controller/StockFileWriter.java | 8 +++++ .../controller/StockInformationWriter.java | 34 ------------------ 3 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 src/main/java/millions/controller/CSVStockFileWriter.java create mode 100644 src/main/java/millions/controller/StockFileWriter.java delete mode 100644 src/main/java/millions/controller/StockInformationWriter.java diff --git a/src/main/java/millions/controller/CSVStockFileWriter.java b/src/main/java/millions/controller/CSVStockFileWriter.java new file mode 100644 index 0000000..ceb4677 --- /dev/null +++ b/src/main/java/millions/controller/CSVStockFileWriter.java @@ -0,0 +1,35 @@ +package millions.controller; + +import millions.model.Stock; + +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.List; + +public class CSVStockFileWriter implements StockFileWriter { + List stocks; + String finalString; + + public CSVStockFileWriter(List stocks) { + this.stocks = stocks; + } + + @Override + public void formatString() { + StringBuilder builder = new StringBuilder(); + stocks.forEach(stock -> { + builder.append(stock.getSymbol()); + builder.append(","); + builder.append(stock.getCompany()); + builder.append(","); + builder.append(stock.getLatestPriceChange().toString()); + }); + this.finalString = builder.toString(); + } + + @Override + public boolean write(){ + // TODO: handle file creation/file selection when writing to file + return false; + } +} diff --git a/src/main/java/millions/controller/StockFileWriter.java b/src/main/java/millions/controller/StockFileWriter.java new file mode 100644 index 0000000..2e2046d --- /dev/null +++ b/src/main/java/millions/controller/StockFileWriter.java @@ -0,0 +1,8 @@ +package millions.controller; + +import java.util.List; + +public interface StockFileWriter { + public void formatString(); + public boolean write(); +} diff --git a/src/main/java/millions/controller/StockInformationWriter.java b/src/main/java/millions/controller/StockInformationWriter.java deleted file mode 100644 index 692bc75..0000000 --- a/src/main/java/millions/controller/StockInformationWriter.java +++ /dev/null @@ -1,34 +0,0 @@ -package millions.controller; - -import millions.model.Stock; - -import java.io.*; -import java.util.List; - -public class StockInformationWriter { - private final List stocks; - private final File destinationFile; - - public StockInformationWriter(List stocks, File destinationFile) { - this.stocks = stocks; - this.destinationFile = destinationFile; - } - - public void write() { - StringBuilder builder = new StringBuilder(); - for (Stock stock : stocks) { - builder.append(stock.getSymbol()); - builder.append(","); - builder.append(stock.getCompany()); - builder.append(","); - // Unsure if price history or just latest price should be saved - builder.append(stock.getSalesPrice().toPlainString()); - builder.append("\n"); - } - try (FileWriter writer = new FileWriter(destinationFile); BufferedWriter bufferedWriter = new BufferedWriter(writer)) { - bufferedWriter.write(builder.toString()); - } catch (IOException e) { - e.printStackTrace(); - } - } -} From 764750a5f504b7016a22f15788ba28a684489d4f Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 15:38:45 +0200 Subject: [PATCH 31/83] moved IO files to their own package --- .../millions/controller/{ => fileIO}/CSVStockFileParser.java | 3 +-- .../millions/controller/{ => fileIO}/CSVStockFileWriter.java | 4 +--- .../millions/controller/{ => fileIO}/StockFileWriter.java | 4 +--- .../controller/{ => fileIO}/StockInformationReader.java | 5 +---- 4 files changed, 4 insertions(+), 12 deletions(-) rename src/main/java/millions/controller/{ => fileIO}/CSVStockFileParser.java (94%) rename src/main/java/millions/controller/{ => fileIO}/CSVStockFileWriter.java (89%) rename src/main/java/millions/controller/{ => fileIO}/StockFileWriter.java (63%) rename src/main/java/millions/controller/{ => fileIO}/StockInformationReader.java (85%) diff --git a/src/main/java/millions/controller/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java similarity index 94% rename from src/main/java/millions/controller/CSVStockFileParser.java rename to src/main/java/millions/controller/fileIO/CSVStockFileParser.java index fc39da8..15742db 100644 --- a/src/main/java/millions/controller/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -1,8 +1,7 @@ -package millions.controller; +package millions.controller.fileIO; import millions.model.Stock; -import javax.swing.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/millions/controller/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java similarity index 89% rename from src/main/java/millions/controller/CSVStockFileWriter.java rename to src/main/java/millions/controller/fileIO/CSVStockFileWriter.java index ceb4677..7f602ba 100644 --- a/src/main/java/millions/controller/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java @@ -1,9 +1,7 @@ -package millions.controller; +package millions.controller.fileIO; import millions.model.Stock; -import java.io.FileWriter; -import java.util.ArrayList; import java.util.List; public class CSVStockFileWriter implements StockFileWriter { diff --git a/src/main/java/millions/controller/StockFileWriter.java b/src/main/java/millions/controller/fileIO/StockFileWriter.java similarity index 63% rename from src/main/java/millions/controller/StockFileWriter.java rename to src/main/java/millions/controller/fileIO/StockFileWriter.java index 2e2046d..6b61251 100644 --- a/src/main/java/millions/controller/StockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/StockFileWriter.java @@ -1,6 +1,4 @@ -package millions.controller; - -import java.util.List; +package millions.controller.fileIO; public interface StockFileWriter { public void formatString(); diff --git a/src/main/java/millions/controller/StockInformationReader.java b/src/main/java/millions/controller/fileIO/StockInformationReader.java similarity index 85% rename from src/main/java/millions/controller/StockInformationReader.java rename to src/main/java/millions/controller/fileIO/StockInformationReader.java index c942461..38a2173 100644 --- a/src/main/java/millions/controller/StockInformationReader.java +++ b/src/main/java/millions/controller/fileIO/StockInformationReader.java @@ -1,9 +1,6 @@ -package millions.controller; - -import millions.model.Stock; +package millions.controller.fileIO; import java.io.*; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; From 3acdb6a6f8da331f7e3c94530b4ec7add431c8d1 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 16:34:20 +0200 Subject: [PATCH 32/83] Changelog renamed StockFileReader Added toString method to stock class Created test for CSVStockFileParser Fixed issue where CSVStockFileParser would only accept incorrect formats --- .../controller/fileIO/CSVStockFileParser.java | 9 +++-- ...mationReader.java => StockFileReader.java} | 10 +++-- src/main/java/millions/model/Stock.java | 5 +++ .../java/millions/CSVStockFileParserTest.java | 40 +++++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) rename src/main/java/millions/controller/fileIO/{StockInformationReader.java => StockFileReader.java} (68%) create mode 100644 src/test/java/millions/CSVStockFileParserTest.java diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index 15742db..536e142 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -19,15 +19,16 @@ public CSVStockFileParser(List lines) { } // returns true if all entries have exactly 3 data points - private static boolean verifyCSV(List lines) { + public boolean verifyCSV(List lines) { return lines.stream() - .filter(l -> !(l.startsWith("#") || l.isEmpty())) - .anyMatch(l -> l.split(",").length != 3); + .filter(l -> !(l.startsWith("#") || l.isBlank())) + .noneMatch(l -> l.split(",").length != 3); + } public List parse() { List stocks = new ArrayList<>(); - lines.forEach(l -> { + lines.stream().filter(l -> !((l.startsWith("#") || l.isBlank()))).forEach(l -> { String[] split = l.split(","); String symbol = split[0]; String company = split[1]; diff --git a/src/main/java/millions/controller/fileIO/StockInformationReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java similarity index 68% rename from src/main/java/millions/controller/fileIO/StockInformationReader.java rename to src/main/java/millions/controller/fileIO/StockFileReader.java index 38a2173..22852cf 100644 --- a/src/main/java/millions/controller/fileIO/StockInformationReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -1,18 +1,20 @@ package millions.controller.fileIO; import java.io.*; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -public class StockInformationReader { - private final File file; +public class StockFileReader { + private final Path filePath; - public StockInformationReader(File file) { - this.file = file; + public StockFileReader(Path path) { + this.filePath = path; } public List readFile() { + File file = new File(filePath.toString()); List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { lines = bufferedReader.readAllLines(); diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index feff8bb..2a67d74 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -73,4 +73,9 @@ public BigDecimal getLatestPriceChange() { return currentPrice.subtract(lastPrice); } + + @Override + public String toString() { + return "Stock [symbol: " + symbol + ", company: " + company + ", prices: " + prices + "]"; + } } diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java new file mode 100644 index 0000000..2b53311 --- /dev/null +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -0,0 +1,40 @@ +package millions; + +import millions.controller.fileIO.CSVStockFileParser; +import millions.controller.fileIO.StockFileReader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CSVStockFileParserTest { + @TempDir + static Path tempDir; + + static Path sharedFile; + + @BeforeAll + public static void setUpTestFile() throws Exception { + sharedFile = Files.createFile(tempDir.resolve("file.csv")); + String string = "# Top 500 US Stocks by Market Cap\n"; + string += "# Ticker,Name,Price\n"; + string += "\n"; + string += "NVDA,Nvidia,191.27\n"; + string += "AAPL,Apple Inc.,276.43\n"; + string += "MSFT,Microsoft,404.68\n"; + Files.writeString(sharedFile, string); + } + + @Test + public void parseStockFileTest(){ + StockFileReader stockFileReader = new StockFileReader(sharedFile); + stockFileReader.readFile().forEach(System.out::println); + + CSVStockFileParser parser = new CSVStockFileParser(stockFileReader.readFile()); + parser.parse().forEach(s -> System.out.println(s.toString())); + } +} From 49ac7b1cdab3583fd230d77cb8d08fbcf6905a4b Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 12 Apr 2026 16:41:05 +0200 Subject: [PATCH 33/83] fixed CSVStockFileParserTest Created StockFileReaderTest --- .../java/millions/CSVStockFileParserTest.java | 5 +-- .../java/millions/StockFileReaderTest.java | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/test/java/millions/StockFileReaderTest.java diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index 2b53311..8cb459b 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -11,6 +11,8 @@ import java.nio.file.Files; import java.nio.file.Path; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class CSVStockFileParserTest { @TempDir static Path tempDir; @@ -32,9 +34,8 @@ public static void setUpTestFile() throws Exception { @Test public void parseStockFileTest(){ StockFileReader stockFileReader = new StockFileReader(sharedFile); - stockFileReader.readFile().forEach(System.out::println); CSVStockFileParser parser = new CSVStockFileParser(stockFileReader.readFile()); - parser.parse().forEach(s -> System.out.println(s.toString())); + assertEquals(3, parser.parse().size()); } } diff --git a/src/test/java/millions/StockFileReaderTest.java b/src/test/java/millions/StockFileReaderTest.java new file mode 100644 index 0000000..c40e4ca --- /dev/null +++ b/src/test/java/millions/StockFileReaderTest.java @@ -0,0 +1,35 @@ +package millions; + +import millions.controller.fileIO.StockFileReader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StockFileReaderTest { + @TempDir + static Path tempDir; + + static Path sharedFile; + @BeforeAll + public static void setUpTestFile() throws Exception { + sharedFile = Files.createFile(tempDir.resolve("file.csv")); + String string = "# Top 500 US Stocks by Market Cap\n"; + string += "# Ticker,Name,Price\n"; + string += "\n"; + string += "NVDA,Nvidia,191.27\n"; + string += "AAPL,Apple Inc.,276.43\n"; + string += "MSFT,Microsoft,404.68\n"; + Files.writeString(sharedFile, string); + } + + @Test + public void testReadStockFile() throws Exception { + StockFileReader stockFileReader = new StockFileReader(sharedFile); + assertEquals(6, stockFileReader.readFile().size()); + } +} From 9f127acb76d2f8c9ae59bb1aad202a94a34c5a4d Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 13 Apr 2026 14:21:59 +0200 Subject: [PATCH 34/83] Fixed CSVStockFileWriter Created CSVStockFileWriterTest Made some small changes to StockFileWriter and StockFileReaderTest --- .../controller/fileIO/CSVStockFileParser.java | 4 +- .../controller/fileIO/CSVStockFileWriter.java | 20 ++++++--- .../controller/fileIO/StockFileWriter.java | 4 +- .../java/millions/CSVStockFileWriterTest.java | 43 +++++++++++++++++++ .../java/millions/StockFileReaderTest.java | 2 +- 5 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/test/java/millions/CSVStockFileWriterTest.java diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index 536e142..5a0cee0 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -28,7 +28,9 @@ public boolean verifyCSV(List lines) { public List parse() { List stocks = new ArrayList<>(); - lines.stream().filter(l -> !((l.startsWith("#") || l.isBlank()))).forEach(l -> { + lines.stream() + .filter(l -> !((l.startsWith("#") || l.isBlank()))) + .forEach(l -> { String[] split = l.split(","); String symbol = split[0]; String company = split[1]; diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java index 7f602ba..463ea04 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java @@ -2,11 +2,15 @@ import millions.model.Stock; +import java.io.BufferedWriter; +import java.io.*; +import java.nio.file.Path; import java.util.List; +//TODO: Validation of data before writing public class CSVStockFileWriter implements StockFileWriter { - List stocks; - String finalString; + private final List stocks; + private String finalString; public CSVStockFileWriter(List stocks) { this.stocks = stocks; @@ -20,14 +24,20 @@ public void formatString() { builder.append(","); builder.append(stock.getCompany()); builder.append(","); - builder.append(stock.getLatestPriceChange().toString()); + builder.append(stock.getSalesPrice().toString()); + builder.append("\n"); }); this.finalString = builder.toString(); } @Override - public boolean write(){ - // TODO: handle file creation/file selection when writing to file + public boolean write(Path path){ + try (FileWriter fw = new FileWriter(path.toString()); BufferedWriter writer = new BufferedWriter(fw);) { + this.formatString(); + writer.write(finalString); + } catch (IOException e) { + e.printStackTrace(); + } return false; } } diff --git a/src/main/java/millions/controller/fileIO/StockFileWriter.java b/src/main/java/millions/controller/fileIO/StockFileWriter.java index 6b61251..32a8e13 100644 --- a/src/main/java/millions/controller/fileIO/StockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/StockFileWriter.java @@ -1,6 +1,8 @@ package millions.controller.fileIO; +import java.nio.file.Path; + public interface StockFileWriter { public void formatString(); - public boolean write(); + public boolean write(Path path); } diff --git a/src/test/java/millions/CSVStockFileWriterTest.java b/src/test/java/millions/CSVStockFileWriterTest.java new file mode 100644 index 0000000..6765f7a --- /dev/null +++ b/src/test/java/millions/CSVStockFileWriterTest.java @@ -0,0 +1,43 @@ +package millions; + +import millions.controller.fileIO.CSVStockFileWriter; +import millions.controller.fileIO.StockFileReader; +import millions.model.Stock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.math.BigDecimal; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CSVStockFileWriterTest { + @TempDir + static Path tempDir; + + List stocks; + + @BeforeEach + void setup() { + Stock s1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s2 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + Stock s3 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + + this.stocks = List.of(s1, s2, s3); + } + + @Test + public void testWrite() { + for(Stock stock : this.stocks) { + System.out.println(stock.toString()); + } + CSVStockFileWriter csvStockFileWriter = new CSVStockFileWriter(stocks); + csvStockFileWriter.write(tempDir.resolve("stocks.csv")); + + StockFileReader stockFileReader = new StockFileReader(tempDir.resolve("stocks.csv")); + assertEquals(3, stockFileReader.readFile().size()); + } +} diff --git a/src/test/java/millions/StockFileReaderTest.java b/src/test/java/millions/StockFileReaderTest.java index c40e4ca..bfb9a8d 100644 --- a/src/test/java/millions/StockFileReaderTest.java +++ b/src/test/java/millions/StockFileReaderTest.java @@ -28,7 +28,7 @@ public static void setUpTestFile() throws Exception { } @Test - public void testReadStockFile() throws Exception { + public void testReadStockFile() { StockFileReader stockFileReader = new StockFileReader(sharedFile); assertEquals(6, stockFileReader.readFile().size()); } From 77700e070715a9eede3ac31f60b83ad3b2d35755 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Fri, 17 Apr 2026 14:10:24 +0200 Subject: [PATCH 35/83] Fixed TransactionCalculatorFactory to actually follow factory design pattern --- .../calculators/PurchaseCalculatorFactory.java | 18 ------------------ .../calculators/SaleCalculatorFactory.java | 16 ---------------- .../TransactionCalculatorFactory.java | 14 +++++++++++--- 3 files changed, 11 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/millions/calculators/PurchaseCalculatorFactory.java delete mode 100644 src/main/java/millions/calculators/SaleCalculatorFactory.java diff --git a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java deleted file mode 100644 index 7f496cf..0000000 --- a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package millions.calculators; - -import millions.Share; - -public class PurchaseCalculatorFactory extends TransactionCalculatorFactory { - Share share; - - @Override - public void setShare(Share share) { - this.share = share; - } - - @Override - public TransactionCalculator createCalculator() { - return new PurchaseCalculator(share); - } - -} diff --git a/src/main/java/millions/calculators/SaleCalculatorFactory.java b/src/main/java/millions/calculators/SaleCalculatorFactory.java deleted file mode 100644 index fd16818..0000000 --- a/src/main/java/millions/calculators/SaleCalculatorFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package millions.calculators; - -import millions.Share; - -public class SaleCalculatorFactory extends TransactionCalculatorFactory { - Share share; - - @Override - public void setShare(Share share) { - this.share = share; - } - @Override - public TransactionCalculator createCalculator() { - return new SaleCalculator(share); - } -} diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index 106ca73..dd79b39 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -2,7 +2,15 @@ import millions.Share; -public abstract class TransactionCalculatorFactory { - public abstract void setShare(Share share); - public abstract TransactionCalculator createCalculator(); +public class TransactionCalculatorFactory { + + private TransactionCalculatorFactory() {} + + public TransactionCalculator createPurchaseCalculator(Share share) { + return new PurchaseCalculator(share); + } + + public TransactionCalculator createSaleCalculator(Share share) { + return new SaleCalculator(share); + } } From c3a9c1786d97ba346c40fb45593435c1be989835 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 08:57:43 +0200 Subject: [PATCH 36/83] Merge --- src/main/java/millions/model/Exchange.java | 19 +++++++++++++++++++ src/main/java/millions/model/Player.java | 12 +++++------- src/main/java/millions/model/Portfolio.java | 10 +++++----- src/main/java/millions/model/Stock.java | 4 ++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index fd4d7c1..c19ed0e 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -2,10 +2,13 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.stream.Collectors; public class Exchange { private String name; @@ -67,6 +70,22 @@ public List findStocks(String searchTerm) { .toList(); } + public List getGainers(int limit) { + Collection stocksCollection = stocks.values(); + return stocksCollection.stream() + .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + public List getLosers(int limit) { + Collection stocksCollection = stocks.values(); + return stocksCollection.stream() + .sorted(Comparator.comparing(Stock::getLatestPriceChange)) + .limit(limit) + .collect(Collectors.toList()); + } + public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 9b8ce1f..9f3a682 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -9,7 +9,7 @@ public class Player { private BigDecimal money; private Portfolio portfolio; private TransactionArchive transactionArchive; - //temporary attribute until a better solution is found + // temporary attribute until a better solution is found public int weeksTraded; public Player(String name, BigDecimal startingMoney) { @@ -42,12 +42,6 @@ public void withdrawMoney(BigDecimal amount) { this.money = this.money.subtract(amount); } - public BigDecimal getNetWorth() { - BigDecimal netWorth = this.money; - netWorth = netWorth.add(this.portfolio.getNetWorth()); - return netWorth; - } - public String getStatus() { String status = "Novice"; BigDecimal netWorth = getNetWorth(); @@ -73,6 +67,10 @@ public Portfolio getPortfolio() { return this.portfolio; } + public BigDecimal getNetWorth() { + return this.money.add(this.portfolio.getNetWorth()); + } + public TransactionArchive getTransactionArchive() { return this.transactionArchive; } diff --git a/src/main/java/millions/model/Portfolio.java b/src/main/java/millions/model/Portfolio.java index f5f99d6..0c4af05 100644 --- a/src/main/java/millions/model/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -1,10 +1,9 @@ package millions.model; -import millions.model.calculators.SaleCalculator; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import millions.model.calculators.SaleCalculator; public class Portfolio { List shares; @@ -32,11 +31,12 @@ public List getShares(String symbol) { } public BigDecimal getNetWorth() { - BigDecimal netWorth = new BigDecimal(0); + BigDecimal total = BigDecimal.ZERO; for (Share share : shares) { - netWorth = netWorth.add(new SaleCalculator(share).calculateTotal()); + BigDecimal value = new SaleCalculator(share).calculateTotal(); + total = total.add(value); } - return netWorth; + return total; } public boolean contains(Share share) { diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 2a67d74..65b70da 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -68,6 +68,10 @@ public BigDecimal getLowestPrice() { } public BigDecimal getLatestPriceChange() { + if (this.prices.size() < 2) { + return BigDecimal.ZERO; + } + BigDecimal currentPrice = this.prices.getLast(); BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); From c72ee469e24a80f200c72e86568d678f8cae5922 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:02:12 +0200 Subject: [PATCH 37/83] Added getLosers and tests for getLosers/Gainers --- src/main/java/millions/model/Exchange.java | 8 +++++ src/main/java/millions/model/Stock.java | 2 +- src/test/java/millions/ExchangeTest.java | 41 +++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index c19ed0e..8f59eb7 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -86,6 +86,14 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } + public List getLosers(int limit) { + List gainers = new ArrayList<>(this.getStocks().values()); + gainers = gainers.stream() + .sorted((s1,s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) + .toList().reversed(); + return gainers.subList(0, limit); + } + public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 65b70da..f470e6b 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -73,7 +73,7 @@ public BigDecimal getLatestPriceChange() { } BigDecimal currentPrice = this.prices.getLast(); - BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); + BigDecimal lastPrice = this.prices.get(this.prices.size() - 1); return currentPrice.subtract(lastPrice); } diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index ca4c2fc..59f9cf9 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -4,11 +4,8 @@ import java.math.BigDecimal; import java.util.List; +import java.util.stream.IntStream; -import millions.model.Exchange; -import millions.model.Player; -import millions.model.Share; -import millions.model.Stock; import org.junit.jupiter.api.Test; class ExchangeTest { @@ -85,4 +82,40 @@ public void testNullsAndInvalid() { assertThrows( IllegalArgumentException.class, () -> exchange.buy("DOGL", player, BigDecimal.valueOf(-2))); } + + @Test + public void testGetGainers() { + Stock s1 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + Stock s2 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s3 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2, s3)); + exchange.advance(); + List gainers = exchange.getGainers(3); + + boolean isSorted = IntStream.range(0, gainers.size() -1) + .allMatch(i -> gainers.get(i).getLatestPriceChange().compareTo(gainers.get(i+1).getLatestPriceChange()) <= 0); + + assertTrue(isSorted); + assertEquals(3, gainers.size()); + } + + @Test + public void testGetLosers() { + Stock s1 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + Stock s2 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s3 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2, s3)); + + List losers = exchange.getLosers(3); + for (Stock s : losers) { + System.out.println(s.getLatestPriceChange()); + } + boolean isSorted = IntStream.range(0, losers.size() -1) + .allMatch(i -> losers.get(i).getLatestPriceChange().compareTo(losers.get(i+1).getLatestPriceChange()) <= 0); + + assertTrue(isSorted); + assertEquals(3, losers.size()); + } } From 31965c861e67104008ec0a588a1e1166ef8cf1fd Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 17 Apr 2026 09:49:33 +0200 Subject: [PATCH 38/83] Removing shell.nix --- shell.nix | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 shell.nix diff --git a/shell.nix b/shell.nix deleted file mode 100644 index a9cc7b4..0000000 --- a/shell.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ pkgs ? import {} }: - -let - jdk = pkgs.jdk25; -in pkgs.mkShell { - buildInputs = [ jdk pkgs.maven ]; - JAVA_HOME = "${jdk}"; -} From 8d15ad4f2e736057c5f8519a6b85e4e828ceb2e7 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 17 Apr 2026 10:07:42 +0200 Subject: [PATCH 39/83] Test: Adding test for price change. Small bugfix --- src/main/java/millions/model/Stock.java | 2 +- src/test/java/millions/StockTest.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index f470e6b..65b70da 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -73,7 +73,7 @@ public BigDecimal getLatestPriceChange() { } BigDecimal currentPrice = this.prices.getLast(); - BigDecimal lastPrice = this.prices.get(this.prices.size() - 1); + BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); return currentPrice.subtract(lastPrice); } diff --git a/src/test/java/millions/StockTest.java b/src/test/java/millions/StockTest.java index 50afb44..452db31 100644 --- a/src/test/java/millions/StockTest.java +++ b/src/test/java/millions/StockTest.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; - +import java.util.List; import millions.model.Stock; import org.junit.jupiter.api.Test; @@ -27,6 +27,15 @@ public void settersAndGetters() { assertEquals("Nvadia", stock.getCompany()); } + @Test + public void testGetPriceChange() { + ArrayList prices = new ArrayList<>(List.of(BigDecimal.valueOf(100), BigDecimal.valueOf(125))); + Stock stock = new Stock("AAPL", "Apple", prices); + assertEquals(BigDecimal.valueOf(25), stock.getLatestPriceChange()); + stock.addNewSalesPrice(BigDecimal.valueOf(155)); + assertEquals(BigDecimal.valueOf(30), stock.getLatestPriceChange()); + } + @Test public void testNullsAndInvalid() { From c0cd5a7b4fb1a7f41f6d7faf0817c1addafedacf Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 08:33:00 +0200 Subject: [PATCH 40/83] Adding shell.nix to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9bf95a9..b68b82e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ build/ ### Mac OS ### .DS_Store + +shell.nix From 8cc72cea2756a23c703bc58da3974def1f33486c Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:04:09 +0200 Subject: [PATCH 41/83] Fix/test" Fix portfolio rounding bug, adding test for that --- .../controller/fileIO/StockFileReader.java | 14 +++++++++++--- .../millions/model/calculators/SaleCalculator.java | 9 ++++++--- src/test/java/millions/PortfolioTest.java | 12 +++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 22852cf..47a0809 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -1,6 +1,10 @@ package millions.controller.fileIO; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -16,8 +20,12 @@ public StockFileReader(Path path) { public List readFile() { File file = new File(filePath.toString()); List lines = new ArrayList<>(); - try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { - lines = bufferedReader.readAllLines(); + try (Reader reader = new FileReader(file); + BufferedReader bufferedReader = new BufferedReader(reader)) { + String line; + while ((line = bufferedReader.readLine()) != null) { + lines.add(line); + } } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 96bc6b1..5adbfaf 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -23,7 +23,7 @@ public BigDecimal calculateGross() { @Override public BigDecimal calculateCommission() { - return this.calculateGross().divide(new BigDecimal("100"), RoundingMode.HALF_UP); + return this.calculateGross().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } @Override @@ -33,11 +33,14 @@ public BigDecimal calculateTax() { this.calculateGross().subtract(this.calculateCommission()).subtract(purchaseCosts); return earnings .multiply(new BigDecimal("30")) - .divide(new BigDecimal("100"), RoundingMode.HALF_UP); + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } @Override public BigDecimal calculateTotal() { - return this.calculateGross().subtract(this.calculateCommission()).subtract(this.calculateTax()); + return this.calculateGross() + .subtract(this.calculateCommission()) + .subtract(this.calculateTax()) + .stripTrailingZeros(); } } diff --git a/src/test/java/millions/PortfolioTest.java b/src/test/java/millions/PortfolioTest.java index 286da55..b151f94 100644 --- a/src/test/java/millions/PortfolioTest.java +++ b/src/test/java/millions/PortfolioTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; - import millions.model.Portfolio; import millions.model.Share; import millions.model.Stock; @@ -56,6 +55,17 @@ public void testGettersAndSetters() { assertEquals(1, portfolio.getShares().size()); } + @Test + public void testGetNetWorth() { + Portfolio portfolio = new Portfolio(); + Stock stock = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, 1, BigDecimal.valueOf(50)); + + portfolio.addShare(share); + + assertEquals(new BigDecimal("84.3"), portfolio.getNetWorth()); + } + @Test public void testNullsAndInvalid() { Stock stock1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); From 6977a2131b5e4c47116b85a6fdbfeb7d314aaad5 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:56:21 +0200 Subject: [PATCH 42/83] Changing to factory pattern for Transaction --- .../TransactionCalculatorFactory.java | 5 +++- src/main/java/millions/model/Exchange.java | 30 ++++++++++--------- src/main/java/millions/model/Player.java | 2 ++ .../model/calculators/SaleCalculator.java | 3 ++ src/test/java/millions/ExchangeTest.java | 2 +- src/test/java/millions/SaleTest.java | 2 +- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index dd79b39..8d461bd 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -1,6 +1,9 @@ package millions.calculators; -import millions.Share; +import millions.model.Share; +import millions.model.calculators.PurchaseCalculator; +import millions.model.calculators.SaleCalculator; +import millions.model.calculators.TransactionCalculator; public class TransactionCalculatorFactory { diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 8f59eb7..46ab5ef 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -9,12 +9,17 @@ import java.util.Map; import java.util.Random; import java.util.stream.Collectors; +import millions.model.factories.PurchaseFactory; +import millions.model.factories.SaleFactory; +import millions.model.factories.TransactionFactory; public class Exchange { private String name; private Map stocks; private int weekNumber; private Random random = new Random(); + private final TransactionFactory purchaseFactory = new PurchaseFactory(); + private final TransactionFactory saleFactory = new SaleFactory(); public Exchange(String name, List stockList) { this.name = name; @@ -31,7 +36,7 @@ public Exchange(String name, List stockList) { } } - public void buy(String symbol, Player player, BigDecimal quantity) { + public Transaction buy(String symbol, Player player, BigDecimal quantity) { Stock stock = this.stocks.get(symbol); if (stock == null) { @@ -39,17 +44,22 @@ public void buy(String symbol, Player player, BigDecimal quantity) { } Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); - Purchase purchase = new Purchase(shareToBuy, this.weekNumber); + + Transaction purchase = purchaseFactory.createTransaction(shareToBuy, weekNumber); purchase.commit(player); + + return purchase; } - public void buy(String symbol, Player player, int quantity) { - this.buy(symbol, player, BigDecimal.valueOf(quantity)); + public Transaction buy(String symbol, Player player, int quantity) { + return this.buy(symbol, player, BigDecimal.valueOf(quantity)); } - public void sell(Share share, Player player) { - Sale sale = new Sale(share, weekNumber); + public Transaction sell(Share share, Player player) { + Transaction sale = saleFactory.createTransaction(share, weekNumber); + sale.commit(player); + return sale; } public Map getStocks() { @@ -86,14 +96,6 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } - public List getLosers(int limit) { - List gainers = new ArrayList<>(this.getStocks().values()); - gainers = gainers.stream() - .sorted((s1,s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) - .toList().reversed(); - return gainers.subList(0, limit); - } - public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 9f3a682..fa323e6 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -43,6 +43,8 @@ public void withdrawMoney(BigDecimal amount) { } public String getStatus() { + int weeksTraded = transactionArchive.countDistinctWeeks(); + String status = "Novice"; BigDecimal netWorth = getNetWorth(); BigDecimal netWorthChange = netWorth.divide(startingMoney, RoundingMode.DOWN); diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 5adbfaf..9b0a84e 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -31,6 +31,9 @@ public BigDecimal calculateTax() { BigDecimal purchaseCosts = this.purchasePrice.multiply(this.quantity); BigDecimal earnings = this.calculateGross().subtract(this.calculateCommission()).subtract(purchaseCosts); + if (earnings.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } return earnings .multiply(new BigDecimal("30")) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index 59f9cf9..b8603c6 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.stream.IntStream; - +import millions.model.*; import org.junit.jupiter.api.Test; class ExchangeTest { diff --git a/src/test/java/millions/SaleTest.java b/src/test/java/millions/SaleTest.java index 1c02005..2d725f4 100644 --- a/src/test/java/millions/SaleTest.java +++ b/src/test/java/millions/SaleTest.java @@ -22,7 +22,7 @@ public void testHappyPath() { sale.commit(player); assertTrue(sale.isCommitted()); - assertEquals(120, player.getMoney().intValue()); + assertEquals(119, player.getMoney().intValue()); assertFalse(player.getPortfolio().getShares().contains(share)); } From df49c3162c4ba29c83314dd491ed637d6da0c708 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 12:22:32 +0200 Subject: [PATCH 43/83] fix: Changed test after changing return. --- src/main/java/millions/model/Player.java | 1 + src/test/java/millions/ExchangeTest.java | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index fa323e6..4324bce 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -43,6 +43,7 @@ public void withdrawMoney(BigDecimal amount) { } public String getStatus() { + // TODO dobbel sjekk logikken int weeksTraded = transactionArchive.countDistinctWeeks(); String status = "Novice"; diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index b8603c6..a17bb50 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -93,8 +93,15 @@ public void testGetGainers() { exchange.advance(); List gainers = exchange.getGainers(3); - boolean isSorted = IntStream.range(0, gainers.size() -1) - .allMatch(i -> gainers.get(i).getLatestPriceChange().compareTo(gainers.get(i+1).getLatestPriceChange()) <= 0); + boolean isSorted = + IntStream.range(0, gainers.size() - 1) + .allMatch( + i -> + gainers + .get(i) + .getLatestPriceChange() + .compareTo(gainers.get(i + 1).getLatestPriceChange()) + >= 0); assertTrue(isSorted); assertEquals(3, gainers.size()); @@ -112,8 +119,15 @@ public void testGetLosers() { for (Stock s : losers) { System.out.println(s.getLatestPriceChange()); } - boolean isSorted = IntStream.range(0, losers.size() -1) - .allMatch(i -> losers.get(i).getLatestPriceChange().compareTo(losers.get(i+1).getLatestPriceChange()) <= 0); + boolean isSorted = + IntStream.range(0, losers.size() - 1) + .allMatch( + i -> + losers + .get(i) + .getLatestPriceChange() + .compareTo(losers.get(i + 1).getLatestPriceChange()) + <= 0); assertTrue(isSorted); assertEquals(3, losers.size()); From 2a120fe83793af11d1d4cb37b696c2f7c582c551 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 09:06:40 +0200 Subject: [PATCH 44/83] feat: Adding Listeners --- src/main/java/millions/model/Exchange.java | 25 +++++++++++++ src/main/java/millions/model/Player.java | 43 ++++++++++++++++++++++ src/main/java/millions/model/Purchase.java | 3 +- src/main/java/millions/model/Sale.java | 2 +- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 46ab5ef..57d91fd 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -20,6 +21,7 @@ public class Exchange { private Random random = new Random(); private final TransactionFactory purchaseFactory = new PurchaseFactory(); private final TransactionFactory saleFactory = new SaleFactory(); + private final List listeners = new ArrayList<>(); public Exchange(String name, List stockList) { this.name = name; @@ -47,6 +49,7 @@ public Transaction buy(String symbol, Player player, BigDecimal quantity) { Transaction purchase = purchaseFactory.createTransaction(shareToBuy, weekNumber); purchase.commit(player); + notifyTransactionCompleted(purchase); return purchase; } @@ -59,6 +62,7 @@ public Transaction sell(Share share, Player player) { Transaction sale = saleFactory.createTransaction(share, weekNumber); sale.commit(player); + notifyTransactionCompleted(sale); return sale; } @@ -107,5 +111,26 @@ public void advance() { .setScale(2, RoundingMode.HALF_UP)); // RoundingMode from AI suggestion } + notifyWeekAdvanced(); + } + + public void addListener(ExchangeListener listener) { + listeners.add(listener); + } + + public void removeListener(ExchangeListener listener) { + listeners.remove(listener); + } + + private void notifyWeekAdvanced() { + for (ExchangeListener listener : listeners) { + listener.onWeekAdvanced(weekNumber); + } + } + + private void notifyTransactionCompleted(Transaction transaction) { + for (ExchangeListener listener : listeners) { + listener.onTransactionCompleted(transaction); + } } } diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 4324bce..09f43d4 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -2,6 +2,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; public class Player { private String name; @@ -11,6 +13,7 @@ public class Player { private TransactionArchive transactionArchive; // temporary attribute until a better solution is found public int weeksTraded; + private final List listeners = new ArrayList<>(); public Player(String name, BigDecimal startingMoney) { this.name = name; @@ -33,6 +36,7 @@ public void addMoney(BigDecimal amount) { throw new IllegalArgumentException("Amount cannot be null or negative"); } this.money = this.money.add(amount); + notifyMoneyChanged(); } public void withdrawMoney(BigDecimal amount) { @@ -40,6 +44,7 @@ public void withdrawMoney(BigDecimal amount) { throw new IllegalArgumentException("Amount cannot be null or negative"); } this.money = this.money.subtract(amount); + notifyMoneyChanged(); } public String getStatus() { @@ -70,6 +75,18 @@ public Portfolio getPortfolio() { return this.portfolio; } + public void addShareToPortfolio(Share share) { + this.portfolio.addShare(share); + notifyPortfolioChanged(); + notifyStatusChanged(); + } + + public void removeShareFromPortfolio(Share share) { + this.portfolio.removeShare(share); + notifyPortfolioChanged(); + notifyStatusChanged(); + } + public BigDecimal getNetWorth() { return this.money.add(this.portfolio.getNetWorth()); } @@ -77,4 +94,30 @@ public BigDecimal getNetWorth() { public TransactionArchive getTransactionArchive() { return this.transactionArchive; } + + public void addListener(PlayerListener listener) { + listeners.add(listener); + } + + public void removeListener(PlayerListener listener) { + listeners.remove(listener); + } + + private void notifyMoneyChanged() { + for (PlayerListener listener : listeners) { + listener.onMoneyChanged(money); + } + } + + private void notifyPortfolioChanged() { + for (PlayerListener listener : listeners) { + listener.onPortfolioChanged(); + } + } + + private void notifyStatusChanged() { + for (PlayerListener listener : listeners) { + listener.onStatusChanged(getStatus()); + } + } } diff --git a/src/main/java/millions/model/Purchase.java b/src/main/java/millions/model/Purchase.java index 70bf608..59da2c0 100644 --- a/src/main/java/millions/model/Purchase.java +++ b/src/main/java/millions/model/Purchase.java @@ -18,7 +18,8 @@ public void commit(Player player) { throw new IllegalStateException("Not enought money"); } player.withdrawMoney(getCalculator().calculateTotal()); - player.getPortfolio().addShare(getShare()); + // Don't reach directly to the portefolio object + player.addShareToPortfolio(getShare()); player.getTransactionArchive().add(this); setCommitted(true); } diff --git a/src/main/java/millions/model/Sale.java b/src/main/java/millions/model/Sale.java index 79ef8b0..dbd3637 100644 --- a/src/main/java/millions/model/Sale.java +++ b/src/main/java/millions/model/Sale.java @@ -18,7 +18,7 @@ public void commit(Player player) { throw new IllegalStateException("Does not own the share"); } player.addMoney(getCalculator().calculateTotal()); - player.getPortfolio().removeShare(getShare()); + player.removeShareFromPortfolio(getShare()); player.getTransactionArchive().add(this); setCommitted(true); } From eee4b84c7d492079213a304635d97899f7920cfb Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 09:08:15 +0200 Subject: [PATCH 45/83] Adding remaining Listerner files and tests --- .../java/millions/model/ExchangeListener.java | 8 ++ .../java/millions/model/PlayerListener.java | 12 +++ .../java/millions/ExchangeListenerTest.java | 98 +++++++++++++++++++ .../java/millions/PlayerListenerTest.java | 96 ++++++++++++++++++ src/test/java/millions/PlayerTest.java | 35 ++++--- 5 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 src/main/java/millions/model/ExchangeListener.java create mode 100644 src/main/java/millions/model/PlayerListener.java create mode 100644 src/test/java/millions/ExchangeListenerTest.java create mode 100644 src/test/java/millions/PlayerListenerTest.java diff --git a/src/main/java/millions/model/ExchangeListener.java b/src/main/java/millions/model/ExchangeListener.java new file mode 100644 index 0000000..b75057e --- /dev/null +++ b/src/main/java/millions/model/ExchangeListener.java @@ -0,0 +1,8 @@ +package millions.model; + +public interface ExchangeListener { + + void onWeekAdvanced(int newWeek); + + void onTransactionCompleted(Transaction transaction); +} diff --git a/src/main/java/millions/model/PlayerListener.java b/src/main/java/millions/model/PlayerListener.java new file mode 100644 index 0000000..f576ac7 --- /dev/null +++ b/src/main/java/millions/model/PlayerListener.java @@ -0,0 +1,12 @@ +package millions.model; + +import java.math.BigDecimal; + +public interface PlayerListener { + + void onMoneyChanged(BigDecimal newBalance); + + void onPortfolioChanged(); + + void onStatusChanged(String newStatus); +} diff --git a/src/test/java/millions/ExchangeListenerTest.java b/src/test/java/millions/ExchangeListenerTest.java new file mode 100644 index 0000000..c47c9e8 --- /dev/null +++ b/src/test/java/millions/ExchangeListenerTest.java @@ -0,0 +1,98 @@ +package millions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import millions.model.Exchange; +import millions.model.ExchangeListener; +import millions.model.Player; +import millions.model.Purchase; +import millions.model.Sale; +import millions.model.Share; +import millions.model.Stock; +import millions.model.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/*** + * Small test class that implements the ExchangeListener. Adds to a list, so that we can easily check lengths and values + */ +class TestExchangeListener implements ExchangeListener { + List weekEvents = new ArrayList<>(); + List transactionEvents = new ArrayList<>(); + + @Override + public void onWeekAdvanced(int newWeek) { + weekEvents.add(newWeek); + } + + @Override + public void onTransactionCompleted(Transaction transaction) { + transactionEvents.add(transaction); + } +} + +class ExchangeListenerTest { + + private Exchange exchange; + private Player player; + private TestExchangeListener listener; + + @BeforeEach + void setUp() { + Stock s1 = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Stock s2 = new Stock("GOOG", "Alphabet Inc.", BigDecimal.valueOf(200)); + Stock s3 = new Stock("NVDA", "NVidia Inc.", BigDecimal.valueOf(200)); + exchange = new Exchange("NASDAQ", List.of(s1, s2, s3)); + player = new Player("TestPlayer", BigDecimal.valueOf(10000)); + + listener = new TestExchangeListener(); + exchange.addListener(listener); + } + + @Test + void advanceNotifiesListener() { + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + assertEquals(2, listener.weekEvents.getFirst()); + + exchange.advance(); + assertEquals(2, listener.weekEvents.size()); + assertEquals(3, listener.weekEvents.get(1)); + } + + @Test + void buyNotifiesListener() { + exchange.buy("AAPL", player, 1); + assertEquals(1, listener.transactionEvents.size()); + assertTrue(listener.transactionEvents.getFirst() instanceof Purchase); + exchange.buy("NVDA", player, 1); + assertTrue( + listener.transactionEvents.getLast().getShare().getStock().getSymbol().equals("NVDA")); + } + + @Test + void sellNotifiesListener() { + exchange.buy("AAPL", player, 1); + listener.transactionEvents.clear(); + + Share share = player.getPortfolio().getShares().getFirst(); + exchange.sell(share, player); + + assertEquals(1, listener.transactionEvents.size()); + assertTrue(listener.transactionEvents.getFirst() instanceof Sale); + } + + @Test + void removeListenerStopsNotifications() { + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + + exchange.removeListener(listener); + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + } +} diff --git a/src/test/java/millions/PlayerListenerTest.java b/src/test/java/millions/PlayerListenerTest.java new file mode 100644 index 0000000..5e2008a --- /dev/null +++ b/src/test/java/millions/PlayerListenerTest.java @@ -0,0 +1,96 @@ +package millions; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import millions.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/*** + * Small test class that implements the PlayerListener. Adds to a list, so that we can easily check lengths and values + */ + +class TestPlayerListener implements PlayerListener { + List moneyEvents = new ArrayList<>(); + int portfolioChangedCount = 0; + List statusEvents = new ArrayList<>(); + + @Override + public void onMoneyChanged(BigDecimal newBalance) { + moneyEvents.add(newBalance); + } + + @Override + public void onPortfolioChanged() { + portfolioChangedCount++; + } + + @Override + public void onStatusChanged(String newStatus) { + statusEvents.add(newStatus); + } +} + +class PlayerListenerTest { + + private Player player; + private TestPlayerListener listener; + + @BeforeEach + void setUp() { + player = new Player("TestPlayer", BigDecimal.valueOf(10000)); + listener = new TestPlayerListener(); + player.addListener(listener); + } + + @Test + void addMoneyNotifiesListener() { + player.addMoney(BigDecimal.valueOf(500)); + assertEquals(1, listener.moneyEvents.size()); + assertEquals(BigDecimal.valueOf(10500), listener.moneyEvents.getFirst()); + } + + @Test + void withdrawMoneyNotifiesListener() { + player.withdrawMoney(BigDecimal.valueOf(300)); + assertEquals(1, listener.moneyEvents.size()); + assertEquals(BigDecimal.valueOf(9700), listener.moneyEvents.getFirst()); + } + + @Test + void addShareNotifiesPortfolioAndStatus() { + Stock stock = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, BigDecimal.valueOf(1), BigDecimal.valueOf(100)); + + player.addShareToPortfolio(share); + assertEquals(1, listener.portfolioChangedCount); + assertEquals(1, listener.statusEvents.size()); + } + + @Test + void removeShareNotifiesPortfolioAndStatus() { + Stock stock = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, BigDecimal.valueOf(1), BigDecimal.valueOf(100)); + + player.addShareToPortfolio(share); + listener.portfolioChangedCount = 0; + listener.statusEvents.clear(); + + player.removeShareFromPortfolio(share); + assertEquals(1, listener.portfolioChangedCount); + assertEquals(1, listener.statusEvents.size()); + } + + @Test + void removeListenerStopsNotifications() { + player.addMoney(BigDecimal.valueOf(100)); + assertEquals(1, listener.moneyEvents.size()); + + player.removeListener(listener); + player.addMoney(BigDecimal.valueOf(100)); + assertEquals(1, listener.moneyEvents.size()); + } +} diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/PlayerTest.java index 839b52c..011a6fb 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/PlayerTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; - import millions.model.Player; import org.junit.jupiter.api.Test; @@ -38,21 +37,21 @@ public void testNullsAndInvalid() { assertThrows(IllegalArgumentException.class, () -> new Player("name", BigDecimal.valueOf(-1))); } - @Test - public void testStatus() { - Player player = new Player("name", BigDecimal.valueOf(1000)); - assertEquals("Novice", player.getStatus()); - - player.addMoney(BigDecimal.valueOf(200)); - assertEquals("Novice", player.getStatus()); - - player.weeksTraded = 10; - assertEquals("Investor", player.getStatus()); - - player.addMoney(BigDecimal.valueOf(200)); - assertEquals("Investor", player.getStatus()); - - player.weeksTraded = 20; - assertEquals("Speculator", player.getStatus()); - } + // @Test + // public void testStatus() { + // Player player = new Player("name", BigDecimal.valueOf(1000)); + // assertEquals("Novice", player.getStatus()); + // + // player.addMoney(BigDecimal.valueOf(200)); + // assertEquals("Novice", player.getStatus()); + // + // player.weeksTraded = 10; + // assertEquals("Investor", player.getStatus()); + // + // player.addMoney(BigDecimal.valueOf(200)); + // assertEquals("Investor", player.getStatus()); + // + // player.weeksTraded = 20; + // assertEquals("Speculator", player.getStatus()); + // } } From b5adc64bd60553ffba8a359206bec3211bc719f9 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 10:21:32 +0200 Subject: [PATCH 46/83] feat: Adding forgotten factory files --- .../java/millions/model/factories/PurchaseFactory.java | 10 ++++++++++ .../java/millions/model/factories/SaleFactory.java | 10 ++++++++++ .../millions/model/factories/TransactionFactory.java | 8 ++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/main/java/millions/model/factories/PurchaseFactory.java create mode 100644 src/main/java/millions/model/factories/SaleFactory.java create mode 100644 src/main/java/millions/model/factories/TransactionFactory.java diff --git a/src/main/java/millions/model/factories/PurchaseFactory.java b/src/main/java/millions/model/factories/PurchaseFactory.java new file mode 100644 index 0000000..e2b52f7 --- /dev/null +++ b/src/main/java/millions/model/factories/PurchaseFactory.java @@ -0,0 +1,10 @@ +package millions.model.factories; + +import millions.model.Purchase; +import millions.model.Share; + +public class PurchaseFactory extends TransactionFactory { + public Purchase createTransaction(Share share, int week) { + return new Purchase(share, week); + } +} diff --git a/src/main/java/millions/model/factories/SaleFactory.java b/src/main/java/millions/model/factories/SaleFactory.java new file mode 100644 index 0000000..4da40ee --- /dev/null +++ b/src/main/java/millions/model/factories/SaleFactory.java @@ -0,0 +1,10 @@ +package millions.model.factories; + +import millions.model.Sale; +import millions.model.Share; + +public class SaleFactory extends TransactionFactory { + public Sale createTransaction(Share share, int week) { + return new Sale(share, week); + } +} diff --git a/src/main/java/millions/model/factories/TransactionFactory.java b/src/main/java/millions/model/factories/TransactionFactory.java new file mode 100644 index 0000000..8c2eb5c --- /dev/null +++ b/src/main/java/millions/model/factories/TransactionFactory.java @@ -0,0 +1,8 @@ +package millions.model.factories; + +import millions.model.Share; +import millions.model.Transaction; + +public abstract class TransactionFactory { + public abstract Transaction createTransaction(Share share, int week); +} From 2c0a768d447c86e816c43670cfb1c23056f3a9b2 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 11:48:37 +0200 Subject: [PATCH 47/83] Starting javaFX code --- pom.xml | 2 +- src/main/java/millions/App.java | 25 +++++ .../millions/controller/GameController.java | 33 +++++++ src/main/java/millions/view/StartView.java | 94 +++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/main/java/millions/App.java create mode 100644 src/main/java/millions/controller/GameController.java create mode 100644 src/main/java/millions/view/StartView.java diff --git a/pom.xml b/pom.xml index 704507d..3a9450d 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ javafx-maven-plugin 0.0.8 - temppackage.Main + millions.App diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java new file mode 100644 index 0000000..825265b --- /dev/null +++ b/src/main/java/millions/App.java @@ -0,0 +1,25 @@ +package millions; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; +import millions.controller.GameController; +import millions.view.StartView; + +public class App extends Application { + + @Override + public void start(Stage stage) { + GameController controller = new GameController(); + StartView startView = new StartView(stage); + + Scene scene = new Scene(startView, 400, 350); + stage.setTitle("Millions"); + stage.setScene(scene); + stage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java new file mode 100644 index 0000000..075230c --- /dev/null +++ b/src/main/java/millions/controller/GameController.java @@ -0,0 +1,33 @@ +package millions.controller; + +import java.math.BigDecimal; +import java.nio.file.Path; +import java.util.List; +import millions.controller.fileIO.CSVStockFileParser; +import millions.controller.fileIO.StockFileReader; +import millions.model.Exchange; +import millions.model.Player; +import millions.model.Stock; + +public class GameController { + private Player player; + private Exchange exchange; + + public void startGame(String name, BigDecimal startingMoney, Path stockFilePath) { + StockFileReader reader = new StockFileReader(stockFilePath); + List lines = reader.readFile(); + CSVStockFileParser parser = new CSVStockFileParser(lines); + List stocks = parser.parse(); + + player = new Player(name, startingMoney); + exchange = new Exchange("Exchange", stocks); + } + + public Player getPlayer() { + return player; + } + + public Exchange getExchange() { + return exchange; + } +} diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java new file mode 100644 index 0000000..564883b --- /dev/null +++ b/src/main/java/millions/view/StartView.java @@ -0,0 +1,94 @@ +package millions.view; + +import java.io.File; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Stage; + +public class StartView extends VBox { + + private TextField nameField; + private TextField startingAmountField; + private File selectedFile; + private Button filepickerButton; + private Button startButton; + + public StartView(Stage stage) { + setAlignment(Pos.CENTER); + setSpacing(12); + setPadding(new Insets(40)); + + nameField = new TextField(); + nameField.setPromptText("Player name:"); + nameField.setMaxWidth(250); + nameField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + + startingAmountField = new TextField(); + startingAmountField.setPromptText("Starting amount:"); + startingAmountField.setMaxWidth(250); + startingAmountField + .textProperty() + .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + + filepickerButton = new Button(); + filepickerButton.setText("Pick file"); + filepickerButton.setMaxWidth(250); + filepickerButton.setOnAction( + e -> { + FileChooser chooser = new FileChooser(); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV files", "*.csv")); + chooser.setTitle("Select stock CSV file"); + File file = chooser.showOpenDialog(stage); + + if (file != null) { + selectedFile = file; + filepickerButton.setText(file.getName()); + } + }); + + startButton = new Button("Start game"); + startButton.setDisable(true); + + Label title = new Label("Millions"); + title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + + getChildren().addAll(title, nameField, startingAmountField, filepickerButton, startButton); + } + + private void checkStartButtonValid() { + boolean valid = true; + + if (nameField.getText().isBlank()) { + valid = false; + } + + try { + Integer.valueOf(startingAmountField.getText()); + } catch (NumberFormatException e) { + valid = false; + } + + startButton.setDisable(!valid); + } + + public String getName() { + return nameField.getText(); + } + + public String getStartingAmount() { + return startingAmountField.getText(); + } + + public File getSelectedFile() { + return selectedFile; + } + + public Button getStartButton() { + return startButton; + } +} From e72b5a45ae787a01835e7cceaa6f021150ed2fea Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 11 May 2026 12:18:45 +0200 Subject: [PATCH 48/83] Docs: Adding javadocs --- src/main/java/millions/App.java | 3 ++ .../TransactionCalculatorFactory.java | 3 ++ .../millions/controller/GameController.java | 1 + .../controller/fileIO/CSVStockFileParser.java | 27 ++++++------ .../controller/fileIO/CSVStockFileWriter.java | 3 ++ .../controller/fileIO/StockFileReader.java | 3 ++ .../controller/fileIO/StockFileWriter.java | 3 ++ src/main/java/millions/model/Exchange.java | 3 ++ .../java/millions/model/ExchangeListener.java | 3 ++ src/main/java/millions/model/Player.java | 44 +++++++++++++++++++ .../java/millions/model/PlayerListener.java | 1 + src/main/java/millions/model/Portfolio.java | 23 ++++++++++ src/main/java/millions/model/Purchase.java | 3 ++ src/main/java/millions/model/Sale.java | 3 ++ src/main/java/millions/model/Share.java | 17 +++++++ src/main/java/millions/model/Stock.java | 32 ++++++++++++++ src/main/java/millions/model/Transaction.java | 1 + .../millions/model/TransactionArchive.java | 1 + .../model/calculators/PurchaseCalculator.java | 1 + .../model/calculators/SaleCalculator.java | 1 + .../calculators/TransactionCalculator.java | 3 ++ .../model/factories/PurchaseFactory.java | 3 ++ .../millions/model/factories/SaleFactory.java | 3 ++ .../model/factories/TransactionFactory.java | 3 ++ src/main/java/millions/view/StartView.java | 1 + 25 files changed, 175 insertions(+), 14 deletions(-) diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 825265b..8ffc777 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -6,6 +6,9 @@ import millions.controller.GameController; import millions.view.StartView; +/** + * Main JavaFX application entry point for the Millions stock trading game. + */ public class App extends Application { @Override diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index 8d461bd..36a71c0 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -5,6 +5,9 @@ import millions.model.calculators.SaleCalculator; import millions.model.calculators.TransactionCalculator; +/** + * Factory for creating transaction calculators. + */ public class TransactionCalculatorFactory { private TransactionCalculatorFactory() {} diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 075230c..b6e5561 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -9,6 +9,7 @@ import millions.model.Player; import millions.model.Stock; +/** Controls game initialization. */ public class GameController { private Player player; private Exchange exchange; diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index 5a0cee0..e52ebf9 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -1,19 +1,18 @@ package millions.controller.fileIO; -import millions.model.Stock; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import millions.model.Stock; +/** Parses CSV lines into Stock objects. */ public class CSVStockFileParser { private List lines; public CSVStockFileParser(List lines) { if (verifyCSV(lines)) { this.lines = lines; - } - else { + } else { // throw file format error } } @@ -21,22 +20,22 @@ public CSVStockFileParser(List lines) { // returns true if all entries have exactly 3 data points public boolean verifyCSV(List lines) { return lines.stream() - .filter(l -> !(l.startsWith("#") || l.isBlank())) - .noneMatch(l -> l.split(",").length != 3); - + .filter(l -> !(l.startsWith("#") || l.isBlank())) + .noneMatch(l -> l.split(",").length != 3); } public List parse() { List stocks = new ArrayList<>(); lines.stream() .filter(l -> !((l.startsWith("#") || l.isBlank()))) - .forEach(l -> { - String[] split = l.split(","); - String symbol = split[0]; - String company = split[1]; - BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); - }); + .forEach( + l -> { + String[] split = l.split(","); + String symbol = split[0]; + String company = split[1]; + BigDecimal price = new BigDecimal(split[2]); + stocks.add(new Stock(symbol, company, price)); + }); return stocks; } } diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java index 463ea04..52fee6e 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java @@ -8,6 +8,9 @@ import java.util.List; //TODO: Validation of data before writing +/** + * Writes stock data to a CSV file. + */ public class CSVStockFileWriter implements StockFileWriter { private final List stocks; private String finalString; diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 47a0809..31e37ee 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -10,6 +10,9 @@ import java.util.List; +/** + * Reads a file and returns its lines as a list of strings. + */ public class StockFileReader { private final Path filePath; diff --git a/src/main/java/millions/controller/fileIO/StockFileWriter.java b/src/main/java/millions/controller/fileIO/StockFileWriter.java index 32a8e13..cfd1baf 100644 --- a/src/main/java/millions/controller/fileIO/StockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/StockFileWriter.java @@ -2,6 +2,9 @@ import java.nio.file.Path; +/** + * Interface for writing stock data to a file. + */ public interface StockFileWriter { public void formatString(); public boolean write(Path path); diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 57d91fd..869cff6 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -14,6 +14,9 @@ import millions.model.factories.SaleFactory; import millions.model.factories.TransactionFactory; +/** + * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price changes. + */ public class Exchange { private String name; private Map stocks; diff --git a/src/main/java/millions/model/ExchangeListener.java b/src/main/java/millions/model/ExchangeListener.java index b75057e..6731d45 100644 --- a/src/main/java/millions/model/ExchangeListener.java +++ b/src/main/java/millions/model/ExchangeListener.java @@ -1,5 +1,8 @@ package millions.model; +/** + * Listener for exchange events such as week advances and completed transactions. + */ public interface ExchangeListener { void onWeekAdvanced(int newWeek); diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 09f43d4..2dfd684 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +/** Player class. */ public class Player { private String name; private BigDecimal startingMoney; @@ -15,6 +16,11 @@ public class Player { public int weeksTraded; private final List listeners = new ArrayList<>(); + /** + * @param name Name of player + * @param startingMoney Amount of money the player starts with + * @throws IllegalArgumentException + */ public Player(String name, BigDecimal startingMoney) { this.name = name; this.startingMoney = startingMoney; @@ -31,6 +37,10 @@ public Player(String name, BigDecimal startingMoney) { } } + /** + * @param amount How much money to add + * @throws IllegalArgumentException + */ public void addMoney(BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount cannot be null or negative"); @@ -39,6 +49,10 @@ public void addMoney(BigDecimal amount) { notifyMoneyChanged(); } + /** + * @param amount How much money to withdeaw + * @throws IllegalArgumentException + */ public void withdrawMoney(BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount cannot be null or negative"); @@ -47,6 +61,9 @@ public void withdrawMoney(BigDecimal amount) { notifyMoneyChanged(); } + /** + * @return + */ public String getStatus() { // TODO dobbel sjekk logikken int weeksTraded = transactionArchive.countDistinctWeeks(); @@ -63,42 +80,69 @@ public String getStatus() { return status; } + /** + * @return + */ public String getName() { return this.name; } + /** + * @return + */ public BigDecimal getMoney() { return this.money; } + /** + * @return + */ public Portfolio getPortfolio() { return this.portfolio; } + /** + * @param share Share to be added + */ public void addShareToPortfolio(Share share) { this.portfolio.addShare(share); notifyPortfolioChanged(); notifyStatusChanged(); } + /** + * @param share Share to be removed + */ public void removeShareFromPortfolio(Share share) { this.portfolio.removeShare(share); notifyPortfolioChanged(); notifyStatusChanged(); } + /** + * @return + */ public BigDecimal getNetWorth() { return this.money.add(this.portfolio.getNetWorth()); } + /** + * @return + */ public TransactionArchive getTransactionArchive() { return this.transactionArchive; } + /** + * @param listener + */ public void addListener(PlayerListener listener) { listeners.add(listener); } + /** + * @param listener + */ public void removeListener(PlayerListener listener) { listeners.remove(listener); } diff --git a/src/main/java/millions/model/PlayerListener.java b/src/main/java/millions/model/PlayerListener.java index f576ac7..e397ead 100644 --- a/src/main/java/millions/model/PlayerListener.java +++ b/src/main/java/millions/model/PlayerListener.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; +/** Listener for player state changes. */ public interface PlayerListener { void onMoneyChanged(BigDecimal newBalance); diff --git a/src/main/java/millions/model/Portfolio.java b/src/main/java/millions/model/Portfolio.java index 0c4af05..210f784 100644 --- a/src/main/java/millions/model/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -5,6 +5,7 @@ import java.util.List; import millions.model.calculators.SaleCalculator; +/** A collection of shares owned by a player. */ public class Portfolio { List shares; @@ -12,24 +13,42 @@ public Portfolio() { shares = new ArrayList<>(); } + /** + * @param share Share to be added + * @return + */ public boolean addShare(Share share) { return this.shares.add(share); } + /** + * @param share Share to be removed + * @return + */ public boolean removeShare(Share share) { return this.shares.remove(share); } + /** + * @return + */ public List getShares() { return this.shares; } + /** + * @param symbol + * @return + */ public List getShares(String symbol) { return this.shares.stream() .filter(share -> share.getStock().getSymbol().equals(symbol)) .toList(); } + /** + * @return + */ public BigDecimal getNetWorth() { BigDecimal total = BigDecimal.ZERO; for (Share share : shares) { @@ -39,6 +58,10 @@ public BigDecimal getNetWorth() { return total; } + /** + * @param share + * @return + */ public boolean contains(Share share) { return this.shares.contains(share); } diff --git a/src/main/java/millions/model/Purchase.java b/src/main/java/millions/model/Purchase.java index 59da2c0..8bbacb8 100644 --- a/src/main/java/millions/model/Purchase.java +++ b/src/main/java/millions/model/Purchase.java @@ -2,6 +2,9 @@ import millions.model.calculators.PurchaseCalculator; +/** + * A transaction representing the purchase of shares. + */ public class Purchase extends Transaction { public Purchase(Share share, int week) { diff --git a/src/main/java/millions/model/Sale.java b/src/main/java/millions/model/Sale.java index dbd3637..1c17745 100644 --- a/src/main/java/millions/model/Sale.java +++ b/src/main/java/millions/model/Sale.java @@ -2,6 +2,9 @@ import millions.model.calculators.SaleCalculator; +/** + * A transaction representing the sale of shares. + */ public class Sale extends Transaction { public Sale(Share share, int week) { diff --git a/src/main/java/millions/model/Share.java b/src/main/java/millions/model/Share.java index 615dc68..7967b95 100644 --- a/src/main/java/millions/model/Share.java +++ b/src/main/java/millions/model/Share.java @@ -2,11 +2,18 @@ import java.math.BigDecimal; +/** Represents a holding of a specific stock with a quantity and purchase price. */ public class Share { Stock stock; BigDecimal quantity; BigDecimal purchasePrice; + /** + * @param stock Which stock the share is for. + * @param quantity How many stocks + * @param purchasePrice Purchase price of the share + * @throws IllegalArgumentException + */ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.stock = stock; this.quantity = quantity; @@ -23,18 +30,28 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { } } + /** Share() with int quantity */ public Share(Stock stock, int quantity, BigDecimal purchasePrice) { this(stock, BigDecimal.valueOf(quantity), purchasePrice); } + /** + * @return + */ public Stock getStock() { return this.stock; } + /** + * @return + */ public BigDecimal getQuantity() { return this.quantity; } + /** + * @return + */ public BigDecimal getPurchasePrice() { return this.purchasePrice; } diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 65b70da..cc1a23f 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -4,11 +4,18 @@ import java.util.ArrayList; import java.util.List; +/** Stock */ public class Stock { String symbol; String company; List prices; + /** + * @param symbol Stock ticker symbol + * @param company company name + * @param prices List of prices + * @throws IllegalArgumentException + */ public Stock(String symbol, String company, List prices) { this.symbol = symbol; this.company = company; @@ -23,30 +30,49 @@ public Stock(String symbol, String company, List prices) { } } + /** Stock() with single price instead of list */ public Stock(String symbol, String company, BigDecimal initialPrice) { this(symbol, company, new ArrayList<>(List.of(initialPrice))); } + /** + * @return + */ public String getSymbol() { return this.symbol; } + /** + * @return + */ public String getCompany() { return this.company; } + /** + * @return + */ public BigDecimal getSalesPrice() { return this.prices.getLast(); } + /** + * @param price Sales price + */ public void addNewSalesPrice(BigDecimal price) { this.prices.add(price); } + /** + * @return + */ public List getHistoricalPrices() { return this.prices; } + /** + * @return + */ public BigDecimal getHighestPrice() { BigDecimal highestPrice = this.prices.get(0); for (BigDecimal price : this.prices) { @@ -57,6 +83,9 @@ public BigDecimal getHighestPrice() { return highestPrice; } + /** + * @return + */ public BigDecimal getLowestPrice() { BigDecimal lowestPrice = this.prices.get(0); for (BigDecimal price : this.prices) { @@ -67,6 +96,9 @@ public BigDecimal getLowestPrice() { return lowestPrice; } + /** + * @return + */ public BigDecimal getLatestPriceChange() { if (this.prices.size() < 2) { return BigDecimal.ZERO; diff --git a/src/main/java/millions/model/Transaction.java b/src/main/java/millions/model/Transaction.java index 8dfdd4f..6e1e8b1 100644 --- a/src/main/java/millions/model/Transaction.java +++ b/src/main/java/millions/model/Transaction.java @@ -2,6 +2,7 @@ import millions.model.calculators.TransactionCalculator; +/** Abstract base class for stock transactions */ public abstract class Transaction { private Share share; diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 5e8b407..5ae7f9f 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.stream.Collectors; +/** Stores and queries commited transactions. */ public class TransactionArchive { List transactions; diff --git a/src/main/java/millions/model/calculators/PurchaseCalculator.java b/src/main/java/millions/model/calculators/PurchaseCalculator.java index 1f7b341..ab2608d 100644 --- a/src/main/java/millions/model/calculators/PurchaseCalculator.java +++ b/src/main/java/millions/model/calculators/PurchaseCalculator.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import millions.model.Share; +/** Calculates costs for a purchase transaction. Commission */ public class PurchaseCalculator implements TransactionCalculator { BigDecimal purchasePrice; BigDecimal quantity; diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 9b0a84e..a907b46 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -4,6 +4,7 @@ import java.math.RoundingMode; import millions.model.Share; +/** Calculates costs for a sale transaction. Commission and profit tax */ public class SaleCalculator implements TransactionCalculator { BigDecimal purchasePrice; BigDecimal salesPrice; diff --git a/src/main/java/millions/model/calculators/TransactionCalculator.java b/src/main/java/millions/model/calculators/TransactionCalculator.java index d2d3917..9e6ecfb 100644 --- a/src/main/java/millions/model/calculators/TransactionCalculator.java +++ b/src/main/java/millions/model/calculators/TransactionCalculator.java @@ -2,6 +2,9 @@ import java.math.BigDecimal; +/** + * Interface for calculating transaction costs including gross, commission, tax, and total. + */ public interface TransactionCalculator { public BigDecimal calculateGross(); diff --git a/src/main/java/millions/model/factories/PurchaseFactory.java b/src/main/java/millions/model/factories/PurchaseFactory.java index e2b52f7..0af39aa 100644 --- a/src/main/java/millions/model/factories/PurchaseFactory.java +++ b/src/main/java/millions/model/factories/PurchaseFactory.java @@ -3,6 +3,9 @@ import millions.model.Purchase; import millions.model.Share; +/** + * Factory for creating purchase transactions. + */ public class PurchaseFactory extends TransactionFactory { public Purchase createTransaction(Share share, int week) { return new Purchase(share, week); diff --git a/src/main/java/millions/model/factories/SaleFactory.java b/src/main/java/millions/model/factories/SaleFactory.java index 4da40ee..6753a5e 100644 --- a/src/main/java/millions/model/factories/SaleFactory.java +++ b/src/main/java/millions/model/factories/SaleFactory.java @@ -3,6 +3,9 @@ import millions.model.Sale; import millions.model.Share; +/** + * Factory for creating sale transactions. + */ public class SaleFactory extends TransactionFactory { public Sale createTransaction(Share share, int week) { return new Sale(share, week); diff --git a/src/main/java/millions/model/factories/TransactionFactory.java b/src/main/java/millions/model/factories/TransactionFactory.java index 8c2eb5c..e77d31d 100644 --- a/src/main/java/millions/model/factories/TransactionFactory.java +++ b/src/main/java/millions/model/factories/TransactionFactory.java @@ -3,6 +3,9 @@ import millions.model.Share; import millions.model.Transaction; +/** + * Abstract factory for creating transactions. + */ public abstract class TransactionFactory { public abstract Transaction createTransaction(Share share, int week); } diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 564883b..0c85a46 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -10,6 +10,7 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; +/** The initial game setup screen where the player enters their info. */ public class StartView extends VBox { private TextField nameField; From cfbc5bc1640f32a234eab4af705e97f7400a3470 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 11 May 2026 14:04:26 +0200 Subject: [PATCH 49/83] fet: Adding javafx tab view with graph for stocks --- src/main/java/millions/App.java | 29 ++- .../millions/controller/GameController.java | 46 +++- src/main/java/millions/model/Exchange.java | 8 + .../millions/model/TransactionArchive.java | 4 + src/main/java/millions/view/GameView.java | 203 ++++++++++++++++++ src/main/java/millions/view/StartView.java | 37 +++- 6 files changed, 317 insertions(+), 10 deletions(-) create mode 100644 src/main/java/millions/view/GameView.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 8ffc777..107ec61 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,14 +1,14 @@ package millions; +import java.math.BigDecimal; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import millions.controller.GameController; +import millions.view.GameView; import millions.view.StartView; -/** - * Main JavaFX application entry point for the Millions stock trading game. - */ +/** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { @Override @@ -16,6 +16,29 @@ public void start(Stage stage) { GameController controller = new GameController(); StartView startView = new StartView(stage); + startView + .getStartButton() + .setOnAction( + event -> { + try { + controller.startGame( + startView.getName(), + new BigDecimal(startView.getStartingAmount()), + startView.getSelectedFile().toPath(), + startView.getPreRunWeeks()); + + GameView gameView = new GameView(controller); + controller.getPlayer().addListener(gameView); + controller.getExchange().addListener(gameView); + + Scene gameScene = new Scene(gameView, 1920, 1080); + stage.setScene(gameScene); + } catch (RuntimeException ex) { + System.err.println(ex); + System.exit(0); + } + }); + Scene scene = new Scene(startView, 400, 350); stage.setTitle("Millions"); stage.setScene(scene); diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index b6e5561..5638338 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -2,7 +2,9 @@ import java.math.BigDecimal; import java.nio.file.Path; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import millions.controller.fileIO.CSVStockFileParser; import millions.controller.fileIO.StockFileReader; import millions.model.Exchange; @@ -14,14 +16,23 @@ public class GameController { private Player player; private Exchange exchange; - public void startGame(String name, BigDecimal startingMoney, Path stockFilePath) { + public void startGame( + String name, BigDecimal startingMoney, Path stockFilePath, int preRunWeeks) { + if (preRunWeeks < 0) { + throw new IllegalArgumentException("Pre run weeks cannot be negative"); + } + StockFileReader reader = new StockFileReader(stockFilePath); List lines = reader.readFile(); CSVStockFileParser parser = new CSVStockFileParser(lines); List stocks = parser.parse(); - player = new Player(name, startingMoney); exchange = new Exchange("Exchange", stocks); + for (int i = 0; i < preRunWeeks; i++) { + exchange.advance(); + } + + player = new Player(name, startingMoney); } public Player getPlayer() { @@ -31,4 +42,35 @@ public Player getPlayer() { public Exchange getExchange() { return exchange; } + + public List getStocks() { + return exchange.getStocks().values().stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Gives alphabetic sort of findStocks + * + * @param searchTerm + * @return + */ + public List searchStocks(String searchTerm) { + if (searchTerm == null || searchTerm.isBlank()) { + return getStocks(); + } + return exchange.findStocks(searchTerm).stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Get stocks with symbol + * + * @param symbol + * @return + */ + public Stock getStock(String symbol) { + return exchange.getStock(symbol); + } } diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 869cff6..ef59694 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -69,6 +69,14 @@ public Transaction sell(Share share, Player player) { return sale; } + public String getName() { + return this.name; + } + + public int getWeekNumber() { + return this.weekNumber; + } + public Map getStocks() { return this.stocks; } diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 5ae7f9f..6910a6c 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -25,6 +25,10 @@ public boolean isEmpty() { return transactions.isEmpty(); } + public List getTransactions() { + return new ArrayList<>(transactions); + } + public List getTransactions(int week) { return transactions.stream().filter(x -> x.getWeek() == week).collect(Collectors.toList()); } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java new file mode 100644 index 0000000..526c4f3 --- /dev/null +++ b/src/main/java/millions/view/GameView.java @@ -0,0 +1,203 @@ +package millions.view; + +import java.math.BigDecimal; +import java.util.List; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import millions.controller.GameController; +import millions.model.Exchange; +import millions.model.ExchangeListener; +import millions.model.Player; +import millions.model.PlayerListener; +import millions.model.Stock; +import millions.model.Transaction; + +/** Main game screen with tabs */ +public class GameView extends BorderPane implements PlayerListener, ExchangeListener { + + private final GameController controller; + private final Label playerNameLabel = new Label(); + private final Label weekLabel = new Label(); + private final Label moneyLabel = new Label(); + private final Label netWorthLabel = new Label(); + private final Label statusLabel = new Label(); + + private final TextField searchField = new TextField(); + private final ListView stocksList = new ListView<>(); + private final Label selectedStockLabel = new Label("Select a stock to see chart"); + private final NumberAxis xAxis = new NumberAxis(); + private final NumberAxis yAxis = new NumberAxis(); + private final LineChart stockChart = new LineChart<>(xAxis, yAxis); + + public GameView(GameController controller) { + this.controller = controller; + setTop(createHeader()); + setCenter(createTabs()); + configureStocksList(); + refreshAll(); + } + + private HBox createHeader() { + Label title = new Label("Millions"); + title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + + HBox header = + new HBox(20, title, playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel); + return header; + } + + private TabPane createTabs() { + TabPane tabPane = new TabPane(); + tabPane.getTabs().add(createStocksTab()); + tabPane.getTabs().add(createPortfolioTab()); + tabPane.getTabs().add(createTransactionsTab()); + return tabPane; + } + + private Tab createStocksTab() { + VBox leftPane = new VBox(10, new Label("Search"), searchField, stocksList); + + searchField.setPromptText("Search"); + searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshStocks()); + + xAxis.setLabel("Week"); + xAxis.setAutoRanging(false); + xAxis.setLowerBound(1); // Stop week 0 + xAxis.setTickUnit(1); + yAxis.setLabel("Price"); + stockChart.setTitle("Price history"); + stockChart.setLegendVisible(false); + stockChart.setCreateSymbols(true); + stockChart.setAnimated(false); + stockChart.setPrefHeight(500); + + VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + + HBox content = new HBox(12, leftPane, rightPane); + return new Tab("Stocks", content); + } + + private Tab createPortfolioTab() { + VBox content = new VBox(); + return new Tab("Portfolio", content); + } + + private Tab createTransactionsTab() { + VBox content = new VBox(); + return new Tab("Transactions", content); + } + + private void configureStocksList() { + stocksList.setCellFactory( + listView -> + new ListCell<>() { + @Override + protected void updateItem(Stock stock, boolean empty) { + super.updateItem(stock, empty); + if (empty || stock == null) { + setText(null); + } else { + setText(formatStock(stock)); + } + } + }); + + stocksList + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldStock, newStock) -> showStockChart(newStock)); + } + + private void refreshAll() { + refreshPlayerInfo(); + refreshStocks(); + } + + private void refreshPlayerInfo() { + Player player = controller.getPlayer(); + Exchange exchange = controller.getExchange(); + + if (player == null || exchange == null) { + return; + } + + playerNameLabel.setText("Player: " + player.getName()); + weekLabel.setText("Week: " + exchange.getWeekNumber()); + moneyLabel.setText("Money: " + player.getMoney()); + netWorthLabel.setText("Net worth: " + player.getNetWorth()); + statusLabel.setText("Status: " + player.getStatus()); + } + + private void refreshStocks() { + List items = controller.searchStocks(searchField.getText()); + stocksList.getItems().setAll(items); + } + + private void showStockChart(Stock stock) { + stockChart.getData().clear(); + + if (stock == null) { + selectedStockLabel.setText("Select a stock to see chart"); + return; + } + + selectedStockLabel.setText( + stock.getSymbol() + + " - " + + stock.getCompany() + + " | Current: " + + stock.getSalesPrice() + + " | High: " + + stock.getHighestPrice() + + " | Low: " + + stock.getLowestPrice()); + + XYChart.Series series = new XYChart.Series<>(); + List prices = stock.getHistoricalPrices(); + xAxis.setUpperBound(Math.max(2, prices.size())); + for (int i = 0; i < prices.size(); i++) { + series.getData().add(new XYChart.Data<>(i + 1, prices.get(i))); + } + stockChart.getData().add(series); + } + + private String formatStock(Stock stock) { + return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + } + + // Listener callbacks update the shared header and the stocks tab. + @Override + public void onMoneyChanged(BigDecimal newBalance) { + refreshPlayerInfo(); + } + + @Override + public void onPortfolioChanged() { + refreshPlayerInfo(); + } + + @Override + public void onStatusChanged(String newStatus) { + refreshPlayerInfo(); + } + + @Override + public void onWeekAdvanced(int newWeek) { + refreshAll(); + } + + @Override + public void onTransactionCompleted(Transaction transaction) { + refreshPlayerInfo(); + } +} diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 0c85a46..ffccb6f 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -1,6 +1,7 @@ package millions.view; import java.io.File; +import java.math.BigDecimal; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,6 +16,7 @@ public class StartView extends VBox { private TextField nameField; private TextField startingAmountField; + private TextField preRunWeeksField; private File selectedFile; private Button filepickerButton; private Button startButton; @@ -24,17 +26,22 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - nameField = new TextField(); + nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); nameField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); - - startingAmountField = new TextField(); + // Default to 50000 + startingAmountField = new TextField("50000"); startingAmountField.setPromptText("Starting amount:"); startingAmountField.setMaxWidth(250); startingAmountField .textProperty() .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + // Pre run weeks to run simulated weeks before the player starts + preRunWeeksField = new TextField("12"); + preRunWeeksField.setPromptText("Pre run weeks:"); + preRunWeeksField.setMaxWidth(250); + preRunWeeksField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); filepickerButton = new Button(); filepickerButton.setText("Pick file"); @@ -49,6 +56,7 @@ public StartView(Stage stage) { if (file != null) { selectedFile = file; filepickerButton.setText(file.getName()); + checkStartButtonValid(); } }); @@ -58,9 +66,12 @@ public StartView(Stage stage) { Label title = new Label("Millions"); title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); - getChildren().addAll(title, nameField, startingAmountField, filepickerButton, startButton); + getChildren() + .addAll( + title, nameField, startingAmountField, preRunWeeksField, filepickerButton, startButton); } + /** Enables/Disables start button */ private void checkStartButtonValid() { boolean valid = true; @@ -68,8 +79,20 @@ private void checkStartButtonValid() { valid = false; } + if (selectedFile == null) { + valid = false; + } + try { - Integer.valueOf(startingAmountField.getText()); + new BigDecimal(startingAmountField.getText()); + } catch (NumberFormatException e) { + valid = false; + } + + try { + if (Integer.parseInt(preRunWeeksField.getText()) < 0) { + valid = false; + } } catch (NumberFormatException e) { valid = false; } @@ -85,6 +108,10 @@ public String getStartingAmount() { return startingAmountField.getText(); } + public int getPreRunWeeks() { + return Integer.parseInt(preRunWeeksField.getText()); + } + public File getSelectedFile() { return selectedFile; } From 41d75a4fcf8d6c97130f29851fc7941a92aa0e01 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 11 May 2026 15:43:30 +0200 Subject: [PATCH 50/83] changed CSVStockFileParserTest to only test parsing instead of file access --- .../java/millions/CSVStockFileParserTest.java | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index 8cb459b..7683445 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -4,38 +4,33 @@ import millions.controller.fileIO.StockFileReader; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.sql.Array; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; public class CSVStockFileParserTest { - @TempDir - static Path tempDir; - static Path sharedFile; + static String exampleString; @BeforeAll public static void setUpTestFile() throws Exception { - sharedFile = Files.createFile(tempDir.resolve("file.csv")); - String string = "# Top 500 US Stocks by Market Cap\n"; - string += "# Ticker,Name,Price\n"; - string += "\n"; - string += "NVDA,Nvidia,191.27\n"; - string += "AAPL,Apple Inc.,276.43\n"; - string += "MSFT,Microsoft,404.68\n"; - Files.writeString(sharedFile, string); + exampleString = "# Top 500 US Stocks by Market Cap\n"; + exampleString += "# Ticker,Name,Price\n"; + exampleString += "\n"; + exampleString += "NVDA,Nvidia,191.27\n"; + exampleString += "AAPL,Apple Inc.,276.43\n"; + exampleString += "MSFT,Microsoft,404.68\n"; } @Test public void parseStockFileTest(){ - StockFileReader stockFileReader = new StockFileReader(sharedFile); + List testList = List.of(exampleString.split("\n")); - CSVStockFileParser parser = new CSVStockFileParser(stockFileReader.readFile()); + CSVStockFileParser parser = new CSVStockFileParser(testList); assertEquals(3, parser.parse().size()); } } From df559bbf4b0879f3159d5312b651ec871aaae5a7 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 11 May 2026 15:50:52 +0200 Subject: [PATCH 51/83] Created InvalidFormatException to handle incorrect formats when parsing files --- .../millions/controller/fileIO/CSVStockFileParser.java | 2 +- .../millions/controller/fileIO/InvalidFormatException.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/main/java/millions/controller/fileIO/InvalidFormatException.java diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index e52ebf9..a1084ad 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -13,7 +13,7 @@ public CSVStockFileParser(List lines) { if (verifyCSV(lines)) { this.lines = lines; } else { - // throw file format error + throw new InvalidFormatException("Incorrect format for CSV File"); } } diff --git a/src/main/java/millions/controller/fileIO/InvalidFormatException.java b/src/main/java/millions/controller/fileIO/InvalidFormatException.java new file mode 100644 index 0000000..6883256 --- /dev/null +++ b/src/main/java/millions/controller/fileIO/InvalidFormatException.java @@ -0,0 +1,7 @@ +package millions.controller.fileIO; + +public class InvalidFormatException extends RuntimeException { + public InvalidFormatException(String message) { + super(message); + } +} From 932bee60af0ab9b7c9c5c952bd4021a35c4771f9 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Fri, 15 May 2026 15:13:59 +0200 Subject: [PATCH 52/83] Feat: Exception handling for invalid file formats --- src/main/java/millions/App.java | 10 ++++++++ .../millions/controller/GameController.java | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 107ec61..9b459b9 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -3,8 +3,10 @@ import java.math.BigDecimal; import javafx.application.Application; import javafx.scene.Scene; +import javafx.scene.control.Alert; import javafx.stage.Stage; import millions.controller.GameController; +import millions.controller.fileIO.InvalidFormatException; import millions.view.GameView; import millions.view.StartView; @@ -33,6 +35,14 @@ public void start(Stage stage) { Scene gameScene = new Scene(gameView, 1920, 1080); stage.setScene(gameScene); + } catch (InvalidFormatException e) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Error with selected file"); + alert.setContentText("Please control the format of the selected file"); + + alert.showAndWait(); + } catch (RuntimeException ex) { System.err.println(ex); System.exit(0); diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 5638338..fe626bf 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.stream.Collectors; import millions.controller.fileIO.CSVStockFileParser; +import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.StockFileReader; import millions.model.Exchange; import millions.model.Player; @@ -21,18 +22,22 @@ public void startGame( if (preRunWeeks < 0) { throw new IllegalArgumentException("Pre run weeks cannot be negative"); } + try { + StockFileReader reader = new StockFileReader(stockFilePath); + List lines = reader.readFile(); + CSVStockFileParser parser = new CSVStockFileParser(lines); - StockFileReader reader = new StockFileReader(stockFilePath); - List lines = reader.readFile(); - CSVStockFileParser parser = new CSVStockFileParser(lines); - List stocks = parser.parse(); + List stocks = parser.parse(); - exchange = new Exchange("Exchange", stocks); - for (int i = 0; i < preRunWeeks; i++) { - exchange.advance(); - } + exchange = new Exchange("Exchange", stocks); + for (int i = 0; i < preRunWeeks; i++) { + exchange.advance(); + } - player = new Player(name, startingMoney); + player = new Player(name, startingMoney); + } catch(InvalidFormatException e) { + throw new InvalidFormatException(e.getMessage()); + } } public Player getPlayer() { From 4ced67619d884551bd2e86473bff5f7f305a2244 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Fri, 15 May 2026 15:20:54 +0200 Subject: [PATCH 53/83] Added closing prevention to tabs on game view --- src/main/java/millions/view/GameView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 526c4f3..58763d4 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -58,6 +58,7 @@ private HBox createHeader() { private TabPane createTabs() { TabPane tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.getTabs().add(createStocksTab()); tabPane.getTabs().add(createPortfolioTab()); tabPane.getTabs().add(createTransactionsTab()); From 2f63d03d40cc893173e914f60ba0168922b115b3 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 15:04:18 +0200 Subject: [PATCH 54/83] Created CSVFileHandler to reduce overhead code when reading stocks from a CSV file Also changed where file information is passed to filereader and csvparser respectively to accommodate this change --- .../millions/controller/GameController.java | 9 +++--- .../controller/fileIO/CSVFileHandler.java | 31 +++++++++++++++++++ .../controller/fileIO/CSVStockFileParser.java | 15 +++++---- .../controller/fileIO/StockFileReader.java | 9 ++---- .../java/millions/CSVStockFileParserTest.java | 4 +-- .../java/millions/CSVStockFileWriterTest.java | 4 +-- .../java/millions/StockFileReaderTest.java | 4 +-- 7 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 src/main/java/millions/controller/fileIO/CSVFileHandler.java diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index fe626bf..9cc6239 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -5,6 +5,8 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; + +import millions.controller.fileIO.CSVFileHandler; import millions.controller.fileIO.CSVStockFileParser; import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.StockFileReader; @@ -23,11 +25,8 @@ public void startGame( throw new IllegalArgumentException("Pre run weeks cannot be negative"); } try { - StockFileReader reader = new StockFileReader(stockFilePath); - List lines = reader.readFile(); - CSVStockFileParser parser = new CSVStockFileParser(lines); - - List stocks = parser.parse(); + CSVFileHandler fileHandler = new CSVFileHandler(); + List stocks = fileHandler.getStocksFromFile(stockFilePath); exchange = new Exchange("Exchange", stocks); for (int i = 0; i < preRunWeeks; i++) { diff --git a/src/main/java/millions/controller/fileIO/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSVFileHandler.java new file mode 100644 index 0000000..1a8b192 --- /dev/null +++ b/src/main/java/millions/controller/fileIO/CSVFileHandler.java @@ -0,0 +1,31 @@ +package millions.controller.fileIO; + +import millions.controller.fileIO.StockFileReader; +import millions.controller.fileIO.CSVStockFileParser; +import millions.model.Stock; + +import java.nio.file.Path; +import java.util.List; + +public class CSVFileHandler { + StockFileReader reader; + CSVStockFileParser parser; + + public CSVFileHandler() { + this.reader = new StockFileReader(); + this.parser = new CSVStockFileParser(); + } + + public List getStocksFromFile(Path filePath) { + try { + StockFileReader reader = new StockFileReader(); + List lines = reader.readFile(filePath); + + CSVStockFileParser parser = new CSVStockFileParser(); + return parser.parse(lines); + + } catch (InvalidFormatException e) { + throw new InvalidFormatException(e.getMessage()); + } + } +} diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index a1084ad..fa74dde 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -9,13 +9,7 @@ public class CSVStockFileParser { private List lines; - public CSVStockFileParser(List lines) { - if (verifyCSV(lines)) { - this.lines = lines; - } else { - throw new InvalidFormatException("Incorrect format for CSV File"); - } - } + public CSVStockFileParser() {} // returns true if all entries have exactly 3 data points public boolean verifyCSV(List lines) { @@ -24,7 +18,12 @@ public boolean verifyCSV(List lines) { .noneMatch(l -> l.split(",").length != 3); } - public List parse() { + public List parse(List lines) { + if (verifyCSV(lines)) { + this.lines = lines; + } else { + throw new InvalidFormatException("Incorrect format for CSV File"); + } List stocks = new ArrayList<>(); lines.stream() .filter(l -> !((l.startsWith("#") || l.isBlank()))) diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 31e37ee..33956b1 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -14,14 +14,11 @@ * Reads a file and returns its lines as a list of strings. */ public class StockFileReader { - private final Path filePath; - public StockFileReader(Path path) { - this.filePath = path; - } + public StockFileReader() {} - public List readFile() { - File file = new File(filePath.toString()); + public List readFile(Path path) { + File file = new File(path.toString()); List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index 7683445..e81cf60 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -30,7 +30,7 @@ public static void setUpTestFile() throws Exception { public void parseStockFileTest(){ List testList = List.of(exampleString.split("\n")); - CSVStockFileParser parser = new CSVStockFileParser(testList); - assertEquals(3, parser.parse().size()); + CSVStockFileParser parser = new CSVStockFileParser(); + assertEquals(3, parser.parse(testList).size()); } } diff --git a/src/test/java/millions/CSVStockFileWriterTest.java b/src/test/java/millions/CSVStockFileWriterTest.java index 6765f7a..4c156a0 100644 --- a/src/test/java/millions/CSVStockFileWriterTest.java +++ b/src/test/java/millions/CSVStockFileWriterTest.java @@ -37,7 +37,7 @@ public void testWrite() { CSVStockFileWriter csvStockFileWriter = new CSVStockFileWriter(stocks); csvStockFileWriter.write(tempDir.resolve("stocks.csv")); - StockFileReader stockFileReader = new StockFileReader(tempDir.resolve("stocks.csv")); - assertEquals(3, stockFileReader.readFile().size()); + StockFileReader stockFileReader = new StockFileReader(); + assertEquals(3, stockFileReader.readFile(tempDir.resolve("stocks.csv")).size()); } } diff --git a/src/test/java/millions/StockFileReaderTest.java b/src/test/java/millions/StockFileReaderTest.java index bfb9a8d..2bc8970 100644 --- a/src/test/java/millions/StockFileReaderTest.java +++ b/src/test/java/millions/StockFileReaderTest.java @@ -29,7 +29,7 @@ public static void setUpTestFile() throws Exception { @Test public void testReadStockFile() { - StockFileReader stockFileReader = new StockFileReader(sharedFile); - assertEquals(6, stockFileReader.readFile().size()); + StockFileReader stockFileReader = new StockFileReader(); + assertEquals(6, stockFileReader.readFile(sharedFile).size()); } } From ff38fdfdd01d76e32ab253fb5d33d25e3c9e15e9 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 15:09:54 +0200 Subject: [PATCH 55/83] Removed reduntant attributes for CSVParser and changed the parse method to reflect earlier changes --- .../controller/fileIO/CSVStockFileParser.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index fa74dde..530ed02 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -7,7 +7,6 @@ /** Parses CSV lines into Stock objects. */ public class CSVStockFileParser { - private List lines; public CSVStockFileParser() {} @@ -19,22 +18,21 @@ public boolean verifyCSV(List lines) { } public List parse(List lines) { + List stocks = new ArrayList<>(); if (verifyCSV(lines)) { - this.lines = lines; + lines.stream() + .filter(l -> !((l.startsWith("#") || l.isBlank()))) + .forEach( + l -> { + String[] split = l.split(","); + String symbol = split[0]; + String company = split[1]; + BigDecimal price = new BigDecimal(split[2]); + stocks.add(new Stock(symbol, company, price)); + }); } else { throw new InvalidFormatException("Incorrect format for CSV File"); } - List stocks = new ArrayList<>(); - lines.stream() - .filter(l -> !((l.startsWith("#") || l.isBlank()))) - .forEach( - l -> { - String[] split = l.split(","); - String symbol = split[0]; - String company = split[1]; - BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); - }); return stocks; } } From 81ad8ae9fe1edf67ae9e492cb46b6d75a1ac0d95 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 15:12:08 +0200 Subject: [PATCH 56/83] Refactored packagestructure for csv file handlers --- src/main/java/millions/controller/GameController.java | 4 +--- .../millions/controller/fileIO/{ => CSV}/CSVFileHandler.java | 4 ++-- .../controller/fileIO/{ => CSV}/CSVStockFileParser.java | 4 +++- .../controller/fileIO/{ => CSV}/CSVStockFileWriter.java | 3 ++- src/test/java/millions/CSVStockFileParserTest.java | 5 +---- src/test/java/millions/CSVStockFileWriterTest.java | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) rename src/main/java/millions/controller/fileIO/{ => CSV}/CSVFileHandler.java (87%) rename src/main/java/millions/controller/fileIO/{ => CSV}/CSVStockFileParser.java (91%) rename src/main/java/millions/controller/fileIO/{ => CSV}/CSVStockFileWriter.java (92%) diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 9cc6239..f50c4f7 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -6,10 +6,8 @@ import java.util.List; import java.util.stream.Collectors; -import millions.controller.fileIO.CSVFileHandler; -import millions.controller.fileIO.CSVStockFileParser; +import millions.controller.fileIO.CSV.CSVFileHandler; import millions.controller.fileIO.InvalidFormatException; -import millions.controller.fileIO.StockFileReader; import millions.model.Exchange; import millions.model.Player; import millions.model.Stock; diff --git a/src/main/java/millions/controller/fileIO/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java similarity index 87% rename from src/main/java/millions/controller/fileIO/CSVFileHandler.java rename to src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index 1a8b192..5a7d9b8 100644 --- a/src/main/java/millions/controller/fileIO/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -1,7 +1,7 @@ -package millions.controller.fileIO; +package millions.controller.fileIO.CSV; +import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.StockFileReader; -import millions.controller.fileIO.CSVStockFileParser; import millions.model.Stock; import java.nio.file.Path; diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java similarity index 91% rename from src/main/java/millions/controller/fileIO/CSVStockFileParser.java rename to src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 530ed02..2eab5ab 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -1,8 +1,10 @@ -package millions.controller.fileIO; +package millions.controller.fileIO.CSV; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; + +import millions.controller.fileIO.InvalidFormatException; import millions.model.Stock; /** Parses CSV lines into Stock objects. */ diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java similarity index 92% rename from src/main/java/millions/controller/fileIO/CSVStockFileWriter.java rename to src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java index 52fee6e..0233a7c 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java @@ -1,5 +1,6 @@ -package millions.controller.fileIO; +package millions.controller.fileIO.CSV; +import millions.controller.fileIO.StockFileWriter; import millions.model.Stock; import java.io.BufferedWriter; diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index e81cf60..df753aa 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -1,12 +1,9 @@ package millions; -import millions.controller.fileIO.CSVStockFileParser; -import millions.controller.fileIO.StockFileReader; +import millions.controller.fileIO.CSV.CSVStockFileParser; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.sql.Array; -import java.util.ArrayList; import java.util.List; diff --git a/src/test/java/millions/CSVStockFileWriterTest.java b/src/test/java/millions/CSVStockFileWriterTest.java index 4c156a0..c6fe15a 100644 --- a/src/test/java/millions/CSVStockFileWriterTest.java +++ b/src/test/java/millions/CSVStockFileWriterTest.java @@ -1,6 +1,6 @@ package millions; -import millions.controller.fileIO.CSVStockFileWriter; +import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.StockFileReader; import millions.model.Stock; From e2c9b9fb7b2ef0f2a8065b4030c5b85e5c715652 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 16:13:39 +0200 Subject: [PATCH 57/83] Added more robust verification of csv file formats, and expanded on error messages for more clarity on the errors occuring --- src/main/java/millions/App.java | 2 +- .../fileIO/CSV/CSVStockFileParser.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 9b459b9..3d419c3 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -39,7 +39,7 @@ public void start(Stage stage) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); - alert.setContentText("Please control the format of the selected file"); + alert.setContentText(e.getMessage() + "\nPlease control the format of the selected file"); alert.showAndWait(); diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 2eab5ab..eb8339f 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -26,14 +26,20 @@ public List parse(List lines) { .filter(l -> !((l.startsWith("#") || l.isBlank()))) .forEach( l -> { - String[] split = l.split(","); - String symbol = split[0]; - String company = split[1]; - BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); + try { + String[] split = l.split(","); + String symbol = split[0]; + String company = split[1]; + BigDecimal price = new BigDecimal(split[2]); + stocks.add(new Stock(symbol, company, price)); + } catch (NumberFormatException e) { + throw new InvalidFormatException("Error with number conversion on line: " + l + "\n" + "Last field must be a number"); + } catch (IllegalArgumentException e) { + throw new InvalidFormatException("Illegal argument on line: " + l + "\n" + e.getMessage()); + } }); } else { - throw new InvalidFormatException("Incorrect format for CSV File"); + throw new InvalidFormatException("Incorrect format for CSV File: incorrect amount of data fields detected on one or more lines"); } return stocks; } From 25088a1853555143fb5910511375c292c005f5ef Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 16:48:53 +0200 Subject: [PATCH 58/83] Cleaned up CSVStockFileParserTest --- .../java/millions/CSVStockFileParserTest.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index df753aa..aa5fc8c 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -1,33 +1,66 @@ package millions; import millions.controller.fileIO.CSV.CSVStockFileParser; +import millions.controller.fileIO.InvalidFormatException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; + +import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class CSVStockFileParserTest { static String exampleString; + final CSVStockFileParser parser = new CSVStockFileParser(); @BeforeAll - public static void setUpTestFile() throws Exception { + public static void setUpTestString() { exampleString = "# Top 500 US Stocks by Market Cap\n"; exampleString += "# Ticker,Name,Price\n"; exampleString += "\n"; exampleString += "NVDA,Nvidia,191.27\n"; exampleString += "AAPL,Apple Inc.,276.43\n"; exampleString += "MSFT,Microsoft,404.68\n"; + } @Test public void parseStockFileTest(){ List testList = List.of(exampleString.split("\n")); - - CSVStockFileParser parser = new CSVStockFileParser(); assertEquals(3, parser.parse(testList).size()); } + + @Test + public void InvalidFormatExceptionTest() { + exampleString += "Line with incorrect amount of data"; + List testList = List.of(exampleString.split("\n")); + Exception e = assertThrows(InvalidFormatException.class, () -> {parser.parse(testList);}); + String expectedMessage = "Incorrect format for CSV File: incorrect amount of data fields detected on one or more lines"; + assertEquals(expectedMessage, e.getMessage()); + } + + @Test + public void NumberConversionExceptionTest() { + exampleString += "Company, Company Inc., NotANumber"; + List testList = List.of(exampleString.split("\n")); + Exception e = assertThrows(InvalidFormatException.class, () -> {parser.parse(testList);}); + String expectedMessage = "Error with number conversion on line: Company, Company Inc., NotANumber\n" + + "Last field must be a number"; + assertEquals(expectedMessage, e.getMessage()); + } + + @Test + public void EmptyFieldExceptionTest() { + exampleString += ",,1"; + List testList = List.of(exampleString.split("\n")); + Exception e = assertThrows(InvalidFormatException.class, () -> {parser.parse(testList);}); + String expectedMessage = "Illegal argument on line: ,,1\n" + + "Symbol cannot be null or blank"; + assertEquals(expectedMessage, e.getMessage()); + } } From 81a8164874bffe2e5f101dbb4289766fd720b8ab Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 17:01:38 +0200 Subject: [PATCH 59/83] Added additional tests to csv parsing --- .../java/millions/CSVStockFileParserTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/CSVStockFileParserTest.java index aa5fc8c..8266d35 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/CSVStockFileParserTest.java @@ -55,12 +55,21 @@ public void NumberConversionExceptionTest() { } @Test - public void EmptyFieldExceptionTest() { - exampleString += ",,1"; + public void EmptySymbolFieldExceptionTest() { + exampleString += ",test,1"; List testList = List.of(exampleString.split("\n")); Exception e = assertThrows(InvalidFormatException.class, () -> {parser.parse(testList);}); - String expectedMessage = "Illegal argument on line: ,,1\n" + + String expectedMessage = "Illegal argument on line: ,test,1\n" + "Symbol cannot be null or blank"; assertEquals(expectedMessage, e.getMessage()); } + @Test + public void EmptyCompanyNameFieldExceptionTest() { + exampleString += "test,,1"; + List testList = List.of(exampleString.split("\n")); + Exception e = assertThrows(InvalidFormatException.class, () -> {parser.parse(testList);}); + String expectedMessage = "Illegal argument on line: test,,1\n" + + "Company cannot be null or blank"; + assertEquals(expectedMessage, e.getMessage()); + } } From 91ce42c60fbc5751adba5eb4e91bfc0a4ee29c04 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 17:29:08 +0200 Subject: [PATCH 60/83] Added JavaDoc to fileIO files --- .../controller/fileIO/CSV/CSVFileHandler.java | 9 +++++++ .../fileIO/CSV/CSVStockFileParser.java | 20 +++++++++++++- .../fileIO/CSV/CSVStockFileWriter.java | 26 ++++++++++++++----- .../fileIO/InvalidFormatException.java | 3 +++ .../controller/fileIO/StockFileReader.java | 5 ++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index 5a7d9b8..bcd783d 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -7,6 +7,9 @@ import java.nio.file.Path; import java.util.List; +/** + *

Bundles StockFileReader and CSVStockFileParser together to reduce boilerplate code when reading stocks from a csv file

+ */ public class CSVFileHandler { StockFileReader reader; CSVStockFileParser parser; @@ -16,6 +19,12 @@ public CSVFileHandler() { this.parser = new CSVStockFileParser(); } + /** + * Reads and parses stocks from a csv file + * @param filePath Path to stock file + * @return list of stock object created from parsed file + * @throws InvalidFormatException Throws an InvalidFormatException received from parser + */ public List getStocksFromFile(Path filePath) { try { StockFileReader reader = new StockFileReader(); diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index eb8339f..9208bbc 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -12,13 +12,31 @@ public class CSVStockFileParser { public CSVStockFileParser() {} - // returns true if all entries have exactly 3 data points + /** + * Verifies the amount of data fields present in supplied CSV file. + * + * @param lines Lines from csv file + * @return Boolean: True if file satisfies expected format (3 fields) + */ public boolean verifyCSV(List lines) { return lines.stream() .filter(l -> !(l.startsWith("#") || l.isBlank())) .noneMatch(l -> l.split(",").length != 3); } + /** + * Parses the supplied lines if they satisfy the correct format expectations + * + * @param lines

lines to be parsed.
+ * Each line must contain three data fields: String,String,BigDecimal
+ * (Fields cannot be blank)
+ * blank lines or lines beginning with '#' are ignored + *

+ * @return List of stock objects created from the supplied lines + * @throws InvalidFormatException If one or more lines contain too many or too few data fields + * @throws InvalidFormatException If the BigDecimal field on one or more lines are not compatible + * @throws InvalidFormatException Upon recieving an IllegalArgumentException (Either Symbol or Company name is blank) + */ public List parse(List lines) { List stocks = new ArrayList<>(); if (verifyCSV(lines)) { diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java index 0233a7c..3e7a791 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java @@ -1,25 +1,32 @@ package millions.controller.fileIO.CSV; -import millions.controller.fileIO.StockFileWriter; -import millions.model.Stock; - -import java.io.BufferedWriter; import java.io.*; +import java.io.BufferedWriter; import java.nio.file.Path; import java.util.List; +import millions.controller.fileIO.StockFileWriter; +import millions.model.Stock; -//TODO: Validation of data before writing /** - * Writes stock data to a CSV file. + * Implements StockFileWriter.
+ * Converts a list of stock objects into a writeable string with a CSV format. + * */ public class CSVStockFileWriter implements StockFileWriter { private final List stocks; private String finalString; + /** + * Constructor for CSVStockFileWriter + * @param stocks list of stocks to be formatted and written + */ public CSVStockFileWriter(List stocks) { this.stocks = stocks; } + /** + * Formats given string to CSV format to prepare for writing to file + */ @Override public void formatString() { StringBuilder builder = new StringBuilder(); @@ -34,11 +41,18 @@ public void formatString() { this.finalString = builder.toString(); } + /** + * Writes the saved string to a file + * @param path Path to desired file + * @return Boolean for success + */ + // TODO: Disable writing before formatting @Override public boolean write(Path path){ try (FileWriter fw = new FileWriter(path.toString()); BufferedWriter writer = new BufferedWriter(fw);) { this.formatString(); writer.write(finalString); + // TODO: exception handling } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/millions/controller/fileIO/InvalidFormatException.java b/src/main/java/millions/controller/fileIO/InvalidFormatException.java index 6883256..a1e2136 100644 --- a/src/main/java/millions/controller/fileIO/InvalidFormatException.java +++ b/src/main/java/millions/controller/fileIO/InvalidFormatException.java @@ -1,5 +1,8 @@ package millions.controller.fileIO; +/** + * Exception to be thrown when verifying the format of files + */ public class InvalidFormatException extends RuntimeException { public InvalidFormatException(String message) { super(message); diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 33956b1..f873896 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -17,6 +17,11 @@ public class StockFileReader { public StockFileReader() {} + /** + * Reads the file found at the specified path + * @param path Path to the desired file + * @return List of each line in the file as a string + */ public List readFile(Path path) { File file = new File(path.toString()); List lines = new ArrayList<>(); From c71717546a68fc7a08b41ed8cdbfd71641a16833 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 18:01:43 +0200 Subject: [PATCH 61/83] Cleaned up CSVStockFileWriter and added a logger for exceptions Deleted CSVStockFileWriterTest --- src/main/java/millions/App.java | 9 +++- .../fileIO/CSV/CSVStockFileWriter.java | 25 +++++------ .../controller/fileIO/StockFileWriter.java | 7 ++- .../java/millions/CSVStockFileWriterTest.java | 43 ------------------- 4 files changed, 22 insertions(+), 62 deletions(-) delete mode 100644 src/test/java/millions/CSVStockFileWriterTest.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 3d419c3..89fa403 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,18 +1,22 @@ package millions; import java.math.BigDecimal; +import java.util.logging.Level; +import java.util.logging.Logger; + import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.stage.Stage; import millions.controller.GameController; +import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.InvalidFormatException; import millions.view.GameView; import millions.view.StartView; /** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { - + private static final Logger logger = Logger.getLogger(App.class.getName()); @Override public void start(Stage stage) { GameController controller = new GameController(); @@ -36,6 +40,7 @@ public void start(Stage stage) { Scene gameScene = new Scene(gameView, 1920, 1080); stage.setScene(gameScene); } catch (InvalidFormatException e) { + logger.log(Level.FINE, "Invalid format", e); Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); @@ -44,7 +49,7 @@ public void start(Stage stage) { alert.showAndWait(); } catch (RuntimeException ex) { - System.err.println(ex); + logger.log(Level.SEVERE, "Runtime error", ex); System.exit(0); } }); diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java index 3e7a791..fdce35d 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileWriter.java @@ -4,6 +4,8 @@ import java.io.BufferedWriter; import java.nio.file.Path; import java.util.List; +import java.util.logging.Logger; + import millions.controller.fileIO.StockFileWriter; import millions.model.Stock; @@ -13,22 +15,16 @@ * */ public class CSVStockFileWriter implements StockFileWriter { - private final List stocks; + private static final Logger logger = Logger.getLogger(CSVStockFileWriter.class.getName()); private String finalString; - /** - * Constructor for CSVStockFileWriter - * @param stocks list of stocks to be formatted and written - */ - public CSVStockFileWriter(List stocks) { - this.stocks = stocks; - } + public CSVStockFileWriter() {} /** * Formats given string to CSV format to prepare for writing to file */ @Override - public void formatString() { + public String formatString(List stocks) { StringBuilder builder = new StringBuilder(); stocks.forEach(stock -> { builder.append(stock.getSymbol()); @@ -38,23 +34,22 @@ public void formatString() { builder.append(stock.getSalesPrice().toString()); builder.append("\n"); }); - this.finalString = builder.toString(); + return builder.toString(); } /** * Writes the saved string to a file + * @param stocks List of stock objects to write * @param path Path to desired file * @return Boolean for success */ - // TODO: Disable writing before formatting @Override - public boolean write(Path path){ + public boolean write(List stocks, Path path){ try (FileWriter fw = new FileWriter(path.toString()); BufferedWriter writer = new BufferedWriter(fw);) { - this.formatString(); + this.formatString(stocks); writer.write(finalString); - // TODO: exception handling } catch (IOException e) { - e.printStackTrace(); + logger.severe(e.getMessage()); } return false; } diff --git a/src/main/java/millions/controller/fileIO/StockFileWriter.java b/src/main/java/millions/controller/fileIO/StockFileWriter.java index cfd1baf..a85f363 100644 --- a/src/main/java/millions/controller/fileIO/StockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/StockFileWriter.java @@ -1,11 +1,14 @@ package millions.controller.fileIO; +import millions.model.Stock; + import java.nio.file.Path; +import java.util.List; /** * Interface for writing stock data to a file. */ public interface StockFileWriter { - public void formatString(); - public boolean write(Path path); + String formatString(List stocks); + boolean write(List stocks, Path path); } diff --git a/src/test/java/millions/CSVStockFileWriterTest.java b/src/test/java/millions/CSVStockFileWriterTest.java deleted file mode 100644 index c6fe15a..0000000 --- a/src/test/java/millions/CSVStockFileWriterTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package millions; - -import millions.controller.fileIO.CSV.CSVStockFileWriter; -import millions.controller.fileIO.StockFileReader; -import millions.model.Stock; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.math.BigDecimal; -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class CSVStockFileWriterTest { - @TempDir - static Path tempDir; - - List stocks; - - @BeforeEach - void setup() { - Stock s1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); - Stock s2 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); - Stock s3 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); - - this.stocks = List.of(s1, s2, s3); - } - - @Test - public void testWrite() { - for(Stock stock : this.stocks) { - System.out.println(stock.toString()); - } - CSVStockFileWriter csvStockFileWriter = new CSVStockFileWriter(stocks); - csvStockFileWriter.write(tempDir.resolve("stocks.csv")); - - StockFileReader stockFileReader = new StockFileReader(); - assertEquals(3, stockFileReader.readFile(tempDir.resolve("stocks.csv")).size()); - } -} From 6e01d27cccf15c4266f25315b3c6a26270932a01 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 18 May 2026 18:23:13 +0200 Subject: [PATCH 62/83] Added logging for exceptions in App.java --- src/main/java/millions/App.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 89fa403..cb1b7d2 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -40,7 +40,7 @@ public void start(Stage stage) { Scene gameScene = new Scene(gameView, 1920, 1080); stage.setScene(gameScene); } catch (InvalidFormatException e) { - logger.log(Level.FINE, "Invalid format", e); + logger.log(Level.WARNING, "InvalidFormatException: " + e.getMessage()); Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); @@ -49,7 +49,7 @@ public void start(Stage stage) { alert.showAndWait(); } catch (RuntimeException ex) { - logger.log(Level.SEVERE, "Runtime error", ex); + logger.log(Level.SEVERE, ex.getMessage()); System.exit(0); } }); From 2ea3eeecc3abb47881dc567488b8f8cfda13fcfb Mon Sep 17 00:00:00 2001 From: Nikollai Date: Tue, 19 May 2026 16:10:22 +0200 Subject: [PATCH 63/83] Added text input prevention for number input fields in StartView --- src/main/java/millions/view/StartView.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index ffccb6f..f03ac4f 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -7,6 +7,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; @@ -37,11 +38,23 @@ public StartView(Stage stage) { startingAmountField .textProperty() .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + startingAmountField.setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().matches("([0-9]*)?")) { + return change; + } + return null; + })); // Pre run weeks to run simulated weeks before the player starts preRunWeeksField = new TextField("12"); preRunWeeksField.setPromptText("Pre run weeks:"); preRunWeeksField.setMaxWidth(250); preRunWeeksField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + preRunWeeksField.setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().matches("([0-9]*)?")) { + return change; + } + return null; + })); filepickerButton = new Button(); filepickerButton.setText("Pick file"); From fd864790d2c936efe8071094b66d54e9e7927f49 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Tue, 19 May 2026 17:07:19 +0200 Subject: [PATCH 64/83] Added UncheckedFileNotFoundException to handle delegation of FileNotFoundExceptions from reader to view. Added logging in case of unexpected IOExceptions --- src/main/java/millions/App.java | 13 +++++++++++ .../millions/controller/GameController.java | 5 +++++ .../controller/fileIO/CSV/CSVFileHandler.java | 5 +++++ .../controller/fileIO/StockFileReader.java | 22 ++++++++++++------- .../UncheckedFileNotFoundException.java | 7 ++++++ 5 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index cb1b7d2..9a83f2c 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,5 +1,6 @@ package millions; + import java.math.BigDecimal; import java.util.logging.Level; import java.util.logging.Logger; @@ -11,6 +12,7 @@ import millions.controller.GameController; import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.InvalidFormatException; +import millions.controller.fileIO.UncheckedFileNotFoundException; import millions.view.GameView; import millions.view.StartView; @@ -41,6 +43,7 @@ public void start(Stage stage) { stage.setScene(gameScene); } catch (InvalidFormatException e) { logger.log(Level.WARNING, "InvalidFormatException: " + e.getMessage()); + Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); @@ -48,6 +51,16 @@ public void start(Stage stage) { alert.showAndWait(); + } catch(UncheckedFileNotFoundException e) { + logger.log(Level.WARNING, "FileNotFoundException: " + e.getMessage()); + + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Error with selected file"); + alert.setContentText(e.getMessage()); + + alert.showAndWait(); + } catch (RuntimeException ex) { logger.log(Level.SEVERE, ex.getMessage()); System.exit(0); diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index f50c4f7..a151510 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -1,5 +1,6 @@ package millions.controller; + import java.math.BigDecimal; import java.nio.file.Path; import java.util.Comparator; @@ -8,6 +9,7 @@ import millions.controller.fileIO.CSV.CSVFileHandler; import millions.controller.fileIO.InvalidFormatException; +import millions.controller.fileIO.UncheckedFileNotFoundException; import millions.model.Exchange; import millions.model.Player; import millions.model.Stock; @@ -32,6 +34,9 @@ public void startGame( } player = new Player(name, startingMoney); + + } catch(UncheckedFileNotFoundException e) { + throw new UncheckedFileNotFoundException(e.getMessage()); } catch(InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); } diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index bcd783d..fd2c3fb 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -2,8 +2,10 @@ import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.StockFileReader; +import millions.controller.fileIO.UncheckedFileNotFoundException; import millions.model.Stock; + import java.nio.file.Path; import java.util.List; @@ -24,6 +26,7 @@ public CSVFileHandler() { * @param filePath Path to stock file * @return list of stock object created from parsed file * @throws InvalidFormatException Throws an InvalidFormatException received from parser + * @throws UncheckedFileNotFoundException Upon Receiving a FilenotFoundException */ public List getStocksFromFile(Path filePath) { try { @@ -35,6 +38,8 @@ public List getStocksFromFile(Path filePath) { } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); + } catch (UncheckedFileNotFoundException e) { + throw new UncheckedFileNotFoundException(e.getMessage()); } } } diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index f873896..f7121cf 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -1,28 +1,29 @@ package millions.controller.fileIO; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; + + +import java.io.*; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Reads a file and returns its lines as a list of strings. */ public class StockFileReader { - + private static final Logger logger = Logger.getLogger(StockFileReader.class.getName()); public StockFileReader() {} /** * Reads the file found at the specified path * @param path Path to the desired file * @return List of each line in the file as a string + * @throws UncheckedFileNotFoundException Upon encountering a FileNotFoundException */ - public List readFile(Path path) { + public List readFile(Path path) { File file = new File(path.toString()); List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); @@ -32,7 +33,12 @@ public List readFile(Path path) { lines.add(line); } } catch (IOException e) { - e.printStackTrace(); + if (e instanceof FileNotFoundException) { + throw new UncheckedFileNotFoundException("Couldn't find file at specified path"); + } + else { + logger.log(Level.SEVERE, "Encountered unexpected IOException: ", e.getMessage()); + } } return lines; } diff --git a/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java b/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java new file mode 100644 index 0000000..0b4b02d --- /dev/null +++ b/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java @@ -0,0 +1,7 @@ +package millions.controller.fileIO; + +public class UncheckedFileNotFoundException extends RuntimeException { + public UncheckedFileNotFoundException(String message) { + super(message); + } +} From 575acb8917c37f9f535ece335239dbef6df83325 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 22 May 2026 17:37:37 +0200 Subject: [PATCH 65/83] feat: Adding default stocks file --- src/main/java/millions/view/StartView.java | 58 ++++++++++++++++------ src/main/resources/data/default-stocks.csv | 20 ++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/data/default-stocks.csv diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index f03ac4f..44e02f6 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -2,6 +2,10 @@ import java.io.File; import java.math.BigDecimal; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.Objects; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -27,6 +31,9 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); + Label infoField = new Label("Select stock file, or use default"); + // infoField.setMaxWidth(250); + nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); @@ -38,26 +45,32 @@ public StartView(Stage stage) { startingAmountField .textProperty() .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); - startingAmountField.setTextFormatter(new TextFormatter<>(change -> { - if (change.getControlNewText().matches("([0-9]*)?")) { - return change; - } - return null; - })); + startingAmountField.setTextFormatter( + new TextFormatter<>( + change -> { + if (change.getControlNewText().matches("([0-9]*)?")) { + return change; + } + return null; + })); // Pre run weeks to run simulated weeks before the player starts preRunWeeksField = new TextField("12"); preRunWeeksField.setPromptText("Pre run weeks:"); preRunWeeksField.setMaxWidth(250); preRunWeeksField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); - preRunWeeksField.setTextFormatter(new TextFormatter<>(change -> { - if (change.getControlNewText().matches("([0-9]*)?")) { - return change; - } - return null; - })); + preRunWeeksField.setTextFormatter( + new TextFormatter<>( + change -> { + if (change.getControlNewText().matches("([0-9]*)?")) { + return change; + } + return null; + })); + + selectedFile = loadDefaultStocksFile(); filepickerButton = new Button(); - filepickerButton.setText("Pick file"); + filepickerButton.setText("Default stocks"); filepickerButton.setMaxWidth(250); filepickerButton.setOnAction( e -> { @@ -81,7 +94,24 @@ public StartView(Stage stage) { getChildren() .addAll( - title, nameField, startingAmountField, preRunWeeksField, filepickerButton, startButton); + title, + nameField, + startingAmountField, + preRunWeeksField, + infoField, + filepickerButton, + startButton); + + checkStartButtonValid(); + } + + private File loadDefaultStocksFile() { + try { + URL resource = Objects.requireNonNull(getClass().getResource("/data/default-stocks.csv")); + return Paths.get(resource.toURI()).toFile(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Could not load default stocks file", e); + } } /** Enables/Disables start button */ diff --git a/src/main/resources/data/default-stocks.csv b/src/main/resources/data/default-stocks.csv new file mode 100644 index 0000000..4a364c2 --- /dev/null +++ b/src/main/resources/data/default-stocks.csv @@ -0,0 +1,20 @@ +# Default stock data for Millions +# symbol,name,price +AAPL,Pear Inc.,276.43 +MSFT,MacroHard,404.68 +GOOGL,Googly Eyes Inc.,187.34 +AMZN,The Great Bazillion,214.10 +TSLA,Formerly Twitter,342.58 +NVDA,GPUs,191.27 +META,Still Facebook,593.11 +NFLX,And Chill,982.44 +AMD,Advanced Meme Devices,156.79 +JPM,Just Plain Money Chase,245.33 +WFC,Wealthy Folks Credit,82.15 +BAC,Bank of Awkward Capital,48.92 +DIS,Disknee,112.55 +KO,Cocaine,67.14 +PEP,Pepe Cola Co.,159.03 +IBM,Incredibly Boring Machines,241.07 +ORCL,Databases?,171.62 +SAP,Sadly Applying Patches,292.88 From 9af3c761f4ee7be542390c2cd094262b9eb08a80 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 22 May 2026 19:17:00 +0200 Subject: [PATCH 66/83] Feat: Adding buy and sell buttons on stocks tab --- .../millions/controller/GameController.java | 64 +++- src/main/java/millions/view/GameView.java | 289 ++++++++++++++++-- 2 files changed, 326 insertions(+), 27 deletions(-) diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index a151510..5f4a04c 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -1,18 +1,22 @@ package millions.controller; - import java.math.BigDecimal; +import java.math.RoundingMode; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.math.RoundingMode; import java.util.stream.Collectors; - import millions.controller.fileIO.CSV.CSVFileHandler; import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.UncheckedFileNotFoundException; import millions.model.Exchange; import millions.model.Player; +import millions.model.Share; import millions.model.Stock; +import millions.model.Transaction; +import millions.model.calculators.PurchaseCalculator; /** Controls game initialization. */ public class GameController { @@ -35,9 +39,9 @@ public void startGame( player = new Player(name, startingMoney); - } catch(UncheckedFileNotFoundException e) { + } catch (UncheckedFileNotFoundException e) { throw new UncheckedFileNotFoundException(e.getMessage()); - } catch(InvalidFormatException e) { + } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); } } @@ -80,4 +84,56 @@ public List searchStocks(String searchTerm) { public Stock getStock(String symbol) { return exchange.getStock(symbol); } + + public Transaction buyStock(String symbol, BigDecimal quantity) { + if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + return exchange.buy(symbol, player, quantity); + } + + public Transaction sellShare(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); + } + return exchange.sell(share, player); + } + + public List getOwnedShares(String symbol) { + return new ArrayList<>(player.getPortfolio().getShares(symbol)); + } + + public void advanceWeek() { + exchange.advance(); + } + + public BigDecimal getOwnedQuantity(String symbol) { + return player.getPortfolio().getShares(symbol).stream() + .map(Share::getQuantity) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public int getMaxBuyableQuantity(String symbol) { + Stock stock = getStock(symbol); + if (stock == null || player == null) { + return 0; + } + + BigDecimal money = player.getMoney(); + BigDecimal price = stock.getSalesPrice(); + if (price.compareTo(BigDecimal.ZERO) <= 0) { + return 0; + } + + int upperBound = money.divide(price, 0, RoundingMode.FLOOR).intValue(); + // Loop so we take care of comission/tax stuff + for (int quantity = upperBound; quantity >= 1; quantity--) { + Share share = new Share(stock, quantity, price); + BigDecimal total = new PurchaseCalculator(share).calculateTotal(); + if (total.compareTo(money) <= 0) { + return quantity; + } + } + return 0; + } } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 58763d4..51169d1 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -2,15 +2,21 @@ import java.math.BigDecimal; import java.util.List; +import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; @@ -19,6 +25,7 @@ import millions.model.ExchangeListener; import millions.model.Player; import millions.model.PlayerListener; +import millions.model.Share; import millions.model.Stock; import millions.model.Transaction; @@ -35,15 +42,26 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); private final Label selectedStockLabel = new Label("Select a stock to see chart"); + private final Label ownedQuantityLabel = new Label("Owned: 0"); + private final Label actionStatusLabel = new Label(); + private final TextField quantityField = new TextField("1"); + private final Slider quantitySlider = new Slider(1, 1, 1); + private final ComboBox ownedSharesBox = new ComboBox<>(); private final NumberAxis xAxis = new NumberAxis(); private final NumberAxis yAxis = new NumberAxis(); private final LineChart stockChart = new LineChart<>(xAxis, yAxis); + private final Button buyButton = new Button("Buy"); + private final Button sellButton = new Button("Sell"); + private final Button advanceButton = new Button("Advance week"); + private boolean updatingQuantityControls; public GameView(GameController controller) { this.controller = controller; setTop(createHeader()); setCenter(createTabs()); configureStocksList(); + configureButtons(); + configureQuantityControls(); refreshAll(); } @@ -60,8 +78,8 @@ private TabPane createTabs() { TabPane tabPane = new TabPane(); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.getTabs().add(createStocksTab()); - tabPane.getTabs().add(createPortfolioTab()); - tabPane.getTabs().add(createTransactionsTab()); + tabPane.getTabs().add(new Tab("Portfolio", new VBox())); + tabPane.getTabs().add(new Tab("Transactions", new VBox())); return tabPane; } @@ -84,18 +102,72 @@ private Tab createStocksTab() { VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + quantityField.setPrefWidth(70); + quantityField.setTextFormatter( + new TextFormatter<>( + change -> change.getControlNewText().matches("-?\\d*") ? change : null)); + quantitySlider.setPrefWidth(200); + + ownedSharesBox.setPrefWidth(260); + ownedSharesBox.setCellFactory( + comboBox -> + new ListCell<>() { + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + if (empty || share == null) { + setText(null); + setStyle(""); + } else { + setText(formatOwnedShare(share)); + setStyle(getProfitStyle(share)); + } + } + }); + ownedSharesBox.setButtonCell( + new ListCell<>() { + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + if (empty || share == null) { + setText("Select lot"); + setStyle(""); + } else { + setText(formatOwnedShare(share)); + setStyle(getProfitStyle(share)); + } + } + }); + + HBox sellBox = new HBox(8, sellButton, new VBox(4, new Label("Owned Shares"), ownedSharesBox)); + + HBox actionBar = + new HBox( + 10, + ownedQuantityLabel, + quantityField, + quantitySlider, + buyButton, + sellBox, + advanceButton); HBox content = new HBox(12, leftPane, rightPane); - return new Tab("Stocks", content); + VBox outer = new VBox(12, content, actionBar, actionStatusLabel); + return new Tab("Stocks", outer); } - private Tab createPortfolioTab() { - VBox content = new VBox(); - return new Tab("Portfolio", content); + private void configureButtons() { + buyButton.setOnAction(event -> buySelectedStock()); + sellButton.setOnAction(event -> sellSelectedShare()); + advanceButton.setOnAction(event -> advanceWeek()); } - private Tab createTransactionsTab() { - VBox content = new VBox(); - return new Tab("Transactions", content); + private void configureQuantityControls() { + quantityField + .textProperty() + .addListener((obs, oldValue, newValue) -> syncQuantityFromField(newValue)); + quantitySlider + .valueProperty() + .addListener((obs, oldValue, newValue) -> syncQuantityFromSlider(newValue.intValue())); } private void configureStocksList() { @@ -140,8 +212,19 @@ private void refreshPlayerInfo() { } private void refreshStocks() { + Stock selected = stocksList.getSelectionModel().getSelectedItem(); List items = controller.searchStocks(searchField.getText()); stocksList.getItems().setAll(items); + + if (selected != null && items.contains(selected)) { + stocksList.getSelectionModel().select(selected); + showStockChart(selected); + } else if (!items.isEmpty()) { + stocksList.getSelectionModel().selectFirst(); + showStockChart(items.getFirst()); + } else { + showStockChart(null); + } } private void showStockChart(Stock stock) { @@ -149,19 +232,12 @@ private void showStockChart(Stock stock) { if (stock == null) { selectedStockLabel.setText("Select a stock to see chart"); + refreshQuantityControls(null); return; } - selectedStockLabel.setText( - stock.getSymbol() - + " - " - + stock.getCompany() - + " | Current: " - + stock.getSalesPrice() - + " | High: " - + stock.getHighestPrice() - + " | Low: " - + stock.getLowestPrice()); + selectedStockLabel.setText(stock.getSymbol() + " - " + stock.getCompany()); + refreshQuantityControls(stock); XYChart.Series series = new XYChart.Series<>(); List prices = stock.getHistoricalPrices(); @@ -172,6 +248,173 @@ private void showStockChart(Stock stock) { stockChart.getData().add(series); } + private void refreshQuantityControls(Stock stock) { + if (stock == null) { + ownedQuantityLabel.setText("Owned: 0"); + quantityField.setDisable(true); + quantitySlider.setDisable(true); + buyButton.setDisable(true); + sellButton.setDisable(true); + ownedSharesBox.getItems().clear(); + return; + } + + int maxBuyable = controller.getMaxBuyableQuantity(stock.getSymbol()); + int current = clampQuantity(getQuantityValue(), Math.max(1, maxBuyable)); + + updatingQuantityControls = true; + quantityField.setText(String.valueOf(current)); + quantitySlider.setMin(1); + quantitySlider.setMax(Math.max(1, maxBuyable)); + quantitySlider.setValue(current); + quantityField.setDisable(false); + quantitySlider.setDisable(false); + buyButton.setDisable(maxBuyable <= 0); + sellButton.setDisable(false); + updatingQuantityControls = false; + + ownedQuantityLabel.setText("Owned: " + controller.getOwnedQuantity(stock.getSymbol())); + ownedSharesBox.getItems().setAll(controller.getOwnedShares(stock.getSymbol())); + if (!ownedSharesBox.getItems().isEmpty()) { + ownedSharesBox.getSelectionModel().selectFirst(); + } + } + + private void buySelectedStock() { + Stock selectedStock = stocksList.getSelectionModel().getSelectedItem(); + if (selectedStock == null) { + setActionStatus("Select a stock first.", false); + return; + } + + try { + Transaction transaction = + controller.buyStock(selectedStock.getSymbol(), BigDecimal.valueOf(getQuantityValue())); + setActionStatus("Bought " + selectedStock.getSymbol(), true); + refreshAll(); + showStockChart(selectedStock); + } catch (RuntimeException ex) { + setActionStatus(ex.getMessage(), false); + } + } + + private void sellSelectedShare() { + Stock selectedStock = stocksList.getSelectionModel().getSelectedItem(); + Share selectedShare = ownedSharesBox.getSelectionModel().getSelectedItem(); + if (selectedStock == null) { + setActionStatus("Select a stock first.", false); + return; + } + if (selectedShare == null) { + setActionStatus("Select a share lot.", false); + return; + } + + try { + Transaction transaction = controller.sellShare(selectedShare); + setActionStatus("Sold " + selectedStock.getSymbol(), true); + refreshAll(); + showStockChart(selectedStock); + } catch (RuntimeException ex) { + setActionStatus(ex.getMessage(), false); + } + } + + private void advanceWeek() { + controller.advanceWeek(); + refreshAll(); + showStockChart(stocksList.getSelectionModel().getSelectedItem()); + } + + private void setActionStatus(String message, boolean success) { + actionStatusLabel.setText(message); + actionStatusLabel.setStyle(success ? "-fx-text-fill: green;" : "-fx-text-fill: red;"); + } + + private int getQuantityValue() { + String text = quantityField.getText(); + if (text == null || text.isBlank()) { + return (int) Math.round(quantitySlider.getValue()); + } + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + return 1; + } + } + + private void syncQuantityFromField(String newValue) { + if (updatingQuantityControls) { + return; + } + if (newValue == null || newValue.isBlank() || newValue.equals("-")) { + return; + } + + updatingQuantityControls = true; + int clamped = clampQuantity(parseQuantity(newValue), getCurrentMaxBuyable()); + quantityField.setText(String.valueOf(clamped)); + quantitySlider.setValue(clamped); + updatingQuantityControls = false; + } + + private void syncQuantityFromSlider(int newValue) { + if (updatingQuantityControls) { + return; + } + + updatingQuantityControls = true; + int clamped = clampQuantity(newValue, getCurrentMaxBuyable()); + quantityField.setText(String.valueOf(clamped)); + quantitySlider.setValue(clamped); + updatingQuantityControls = false; + } + + private int parseQuantity(String text) { + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + // Just make anything it can't parse to 1 + return 1; + } + } + + private int clampQuantity(int quantity, int maxBuyable) { + int upperBound = Math.max(1, maxBuyable); + if (quantity < 1) { + return 1; + } + if (quantity > upperBound) { + return upperBound; + } + return quantity; + } + + private int getCurrentMaxBuyable() { + Stock selected = stocksList.getSelectionModel().getSelectedItem(); + if (selected == null) { + return 1; + } + return Math.max(1, controller.getMaxBuyableQuantity(selected.getSymbol())); + } + + private String formatOwnedShare(Share share) { + BigDecimal profit = getShareProfit(share); + String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + return share.getQuantity() + "|" + share.getPurchasePrice() + "|" + sign + profit; + } + + private String getProfitStyle(Share share) { + return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 + ? "-fx-text-fill: green;" + : "-fx-text-fill: red;"; + } + + private BigDecimal getShareProfit(Share share) { + BigDecimal currentPrice = share.getStock().getSalesPrice(); + return currentPrice.subtract(share.getPurchasePrice()).multiply(share.getQuantity()); + } + private String formatStock(Stock stock) { return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; } @@ -179,17 +422,17 @@ private String formatStock(Stock stock) { // Listener callbacks update the shared header and the stocks tab. @Override public void onMoneyChanged(BigDecimal newBalance) { - refreshPlayerInfo(); + refreshAll(); } @Override public void onPortfolioChanged() { - refreshPlayerInfo(); + refreshAll(); } @Override public void onStatusChanged(String newStatus) { - refreshPlayerInfo(); + refreshAll(); } @Override @@ -199,6 +442,6 @@ public void onWeekAdvanced(int newWeek) { @Override public void onTransactionCompleted(Transaction transaction) { - refreshPlayerInfo(); + refreshAll(); } } From 8d704be0027411b8aed75eb653767d4e1ac0d5fd Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 22 May 2026 19:17:31 +0200 Subject: [PATCH 67/83] fix: Adding missing staged lines --- src/main/java/millions/controller/GameController.java | 1 - src/main/java/millions/view/GameView.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 5f4a04c..7688f6f 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.math.RoundingMode; import java.util.stream.Collectors; import millions.controller.fileIO.CSV.CSVFileHandler; import millions.controller.fileIO.InvalidFormatException; diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 51169d1..acfecfd 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -2,8 +2,6 @@ import java.math.BigDecimal; import java.util.List; -import javafx.geometry.Insets; -import javafx.geometry.Pos; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; From 53ec0ce70af61a0764b1159e2170e33f306aba18 Mon Sep 17 00:00:00 2001 From: martin Date: Sat, 23 May 2026 10:26:16 +0200 Subject: [PATCH 68/83] refactor: move common gameView functions to ViewUtils --- src/main/java/millions/view/GameView.java | 69 ++----------------- src/main/java/millions/view/ViewUtils.java | 77 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 src/main/java/millions/view/ViewUtils.java diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index acfecfd..a893b04 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -106,36 +106,7 @@ private Tab createStocksTab() { change -> change.getControlNewText().matches("-?\\d*") ? change : null)); quantitySlider.setPrefWidth(200); - ownedSharesBox.setPrefWidth(260); - ownedSharesBox.setCellFactory( - comboBox -> - new ListCell<>() { - @Override - protected void updateItem(Share share, boolean empty) { - super.updateItem(share, empty); - if (empty || share == null) { - setText(null); - setStyle(""); - } else { - setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); - } - } - }); - ownedSharesBox.setButtonCell( - new ListCell<>() { - @Override - protected void updateItem(Share share, boolean empty) { - super.updateItem(share, empty); - if (empty || share == null) { - setText("Select lot"); - setStyle(""); - } else { - setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); - } - } - }); + ViewUtils.configureOwnedSharesBox(ownedSharesBox); HBox sellBox = new HBox(8, sellButton, new VBox(4, new Label("Owned Shares"), ownedSharesBox)); @@ -178,7 +149,7 @@ protected void updateItem(Stock stock, boolean empty) { if (empty || stock == null) { setText(null); } else { - setText(formatStock(stock)); + setText(ViewUtils.formatStock(stock)); } } }); @@ -258,7 +229,7 @@ private void refreshQuantityControls(Stock stock) { } int maxBuyable = controller.getMaxBuyableQuantity(stock.getSymbol()); - int current = clampQuantity(getQuantityValue(), Math.max(1, maxBuyable)); + int current = ViewUtils.clampQuantity(getQuantityValue(), Math.max(1, maxBuyable)); updatingQuantityControls = true; quantityField.setText(String.valueOf(current)); @@ -350,7 +321,7 @@ private void syncQuantityFromField(String newValue) { } updatingQuantityControls = true; - int clamped = clampQuantity(parseQuantity(newValue), getCurrentMaxBuyable()); + int clamped = ViewUtils.clampQuantity(parseQuantity(newValue), getCurrentMaxBuyable()); quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; @@ -362,7 +333,7 @@ private void syncQuantityFromSlider(int newValue) { } updatingQuantityControls = true; - int clamped = clampQuantity(newValue, getCurrentMaxBuyable()); + int clamped = ViewUtils.clampQuantity(newValue, getCurrentMaxBuyable()); quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; @@ -377,17 +348,6 @@ private int parseQuantity(String text) { } } - private int clampQuantity(int quantity, int maxBuyable) { - int upperBound = Math.max(1, maxBuyable); - if (quantity < 1) { - return 1; - } - if (quantity > upperBound) { - return upperBound; - } - return quantity; - } - private int getCurrentMaxBuyable() { Stock selected = stocksList.getSelectionModel().getSelectedItem(); if (selected == null) { @@ -396,25 +356,8 @@ private int getCurrentMaxBuyable() { return Math.max(1, controller.getMaxBuyableQuantity(selected.getSymbol())); } - private String formatOwnedShare(Share share) { - BigDecimal profit = getShareProfit(share); - String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; - return share.getQuantity() + "|" + share.getPurchasePrice() + "|" + sign + profit; - } - - private String getProfitStyle(Share share) { - return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 - ? "-fx-text-fill: green;" - : "-fx-text-fill: red;"; - } - - private BigDecimal getShareProfit(Share share) { - BigDecimal currentPrice = share.getStock().getSalesPrice(); - return currentPrice.subtract(share.getPurchasePrice()).multiply(share.getQuantity()); - } - private String formatStock(Stock stock) { - return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + return ViewUtils.formatStock(stock); } // Listener callbacks update the shared header and the stocks tab. diff --git a/src/main/java/millions/view/ViewUtils.java b/src/main/java/millions/view/ViewUtils.java new file mode 100644 index 0000000..1c8893c --- /dev/null +++ b/src/main/java/millions/view/ViewUtils.java @@ -0,0 +1,77 @@ +package millions.view; + +import java.math.BigDecimal; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ListCell; +import millions.model.Share; +import millions.model.Stock; + +/** Small utility helpers for view formatting and simple calculations. */ +public final class ViewUtils { + + private ViewUtils() {} + + public static void configureOwnedSharesBox(ComboBox comboBox) { + comboBox.setCellFactory( + box -> + new ListCell<>() { + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + if (empty || share == null) { + setText(null); + setStyle(""); + } else { + setText(formatOwnedShare(share)); + setStyle(getProfitStyle(share)); + } + } + }); + comboBox.setButtonCell( + new ListCell<>() { + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + if (empty || share == null) { + setText("Select lot"); + setStyle(""); + } else { + setText(formatOwnedShare(share)); + setStyle(getProfitStyle(share)); + } + } + }); + } + + public static String formatStock(Stock stock) { + return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + } + + public static String formatOwnedShare(Share share) { + BigDecimal profit = getShareProfit(share); + String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + return share.getQuantity() + "|" + share.getPurchasePrice() + "|" + sign + profit; + } + + public static String getProfitStyle(Share share) { + return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 + ? "-fx-text-fill: green;" + : "-fx-text-fill: red;"; + } + + public static BigDecimal getShareProfit(Share share) { + BigDecimal currentPrice = share.getStock().getSalesPrice(); + return currentPrice.subtract(share.getPurchasePrice()).multiply(share.getQuantity()); + } + + public static int clampQuantity(int quantity, int maxBuyable) { + int upperBound = Math.max(1, maxBuyable); + if (quantity < 1) { + return 1; + } + if (quantity > upperBound) { + return upperBound; + } + return quantity; + } +} From 0459af150e1fa63e0a3f71df6475cbda9b6de18d Mon Sep 17 00:00:00 2001 From: martin Date: Sat, 23 May 2026 15:22:55 +0200 Subject: [PATCH 69/83] feat: Adding simple portifolio and transactions list --- src/main/java/millions/view/GameView.java | 43 +++++++++++++++++++++- src/main/java/millions/view/ViewUtils.java | 19 ++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index a893b04..64acd71 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -39,6 +39,8 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); + private final ListView portfolioList = new ListView<>(); + private final ListView transactionsList = new ListView<>(); private final Label selectedStockLabel = new Label("Select a stock to see chart"); private final Label ownedQuantityLabel = new Label("Owned: 0"); private final Label actionStatusLabel = new Label(); @@ -76,8 +78,8 @@ private TabPane createTabs() { TabPane tabPane = new TabPane(); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.getTabs().add(createStocksTab()); - tabPane.getTabs().add(new Tab("Portfolio", new VBox())); - tabPane.getTabs().add(new Tab("Transactions", new VBox())); + tabPane.getTabs().add(createPortfolioTab()); + tabPane.getTabs().add(createTransactionsTab()); return tabPane; } @@ -124,6 +126,16 @@ private Tab createStocksTab() { return new Tab("Stocks", outer); } + private Tab createPortfolioTab() { + portfolioList.setPlaceholder(new Label("No shares yet")); + return new Tab("Portfolio", portfolioList); + } + + private Tab createTransactionsTab() { + transactionsList.setPlaceholder(new Label("No transactions yet")); + return new Tab("Transactions", transactionsList); + } + private void configureButtons() { buyButton.setOnAction(event -> buySelectedStock()); sellButton.setOnAction(event -> sellSelectedShare()); @@ -163,6 +175,8 @@ protected void updateItem(Stock stock, boolean empty) { private void refreshAll() { refreshPlayerInfo(); refreshStocks(); + refreshPortfolio(); + refreshTransactions(); } private void refreshPlayerInfo() { @@ -249,6 +263,31 @@ private void refreshQuantityControls(Stock stock) { } } + private void refreshPortfolio() { + Player player = controller.getPlayer(); + + portfolioList + .getItems() + .setAll( + player.getPortfolio().getShares().stream() + .map(ViewUtils::formatPortfolioShare) + .toList()); + } + + private void refreshTransactions() { + Player player = controller.getPlayer(); + if (player == null) { + return; + } + + transactionsList + .getItems() + .setAll( + player.getTransactionArchive().getTransactions().stream() + .map(ViewUtils::formatTransaction) + .toList()); + } + private void buySelectedStock() { Stock selectedStock = stocksList.getSelectionModel().getSelectedItem(); if (selectedStock == null) { diff --git a/src/main/java/millions/view/ViewUtils.java b/src/main/java/millions/view/ViewUtils.java index 1c8893c..5500438 100644 --- a/src/main/java/millions/view/ViewUtils.java +++ b/src/main/java/millions/view/ViewUtils.java @@ -5,6 +5,7 @@ import javafx.scene.control.ListCell; import millions.model.Share; import millions.model.Stock; +import millions.model.Transaction; /** Small utility helpers for view formatting and simple calculations. */ public final class ViewUtils { @@ -74,4 +75,22 @@ public static int clampQuantity(int quantity, int maxBuyable) { } return quantity; } + + public static String formatPortfolioShare(Share share) { + return share.getStock().getSymbol() + + "|" + + share.getQuantity() + + "|" + + share.getPurchasePrice(); + } + + public static String formatTransaction(Transaction transaction) { + return transaction.getClass().getSimpleName() + + "|" + + transaction.getShare().getStock().getSymbol() + + "|" + + transaction.getShare().getQuantity() + + "|" + + transaction.getWeek(); + } } From 095037295666850300fba2a253f5b70091e466e5 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sat, 23 May 2026 17:54:00 +0200 Subject: [PATCH 70/83] EXPERIMENTAL: Added volatility parameters to stocks --- .../fileIO/CSV/CSVStockFileParser.java | 11 +++- src/main/java/millions/model/Exchange.java | 20 ++++--- src/main/java/millions/model/Stock.java | 18 +++++- .../calculators/PriceChangeCalculator.java | 56 +++++++++++++++++++ 4 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 src/main/java/millions/model/calculators/PriceChangeCalculator.java diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 9208bbc..143cbb0 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -21,7 +21,7 @@ public CSVStockFileParser() {} public boolean verifyCSV(List lines) { return lines.stream() .filter(l -> !(l.startsWith("#") || l.isBlank())) - .noneMatch(l -> l.split(",").length != 3); + .noneMatch(l -> l.split(",").length != 4); } /** @@ -49,9 +49,14 @@ public List parse(List lines) { String symbol = split[0]; String company = split[1]; BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); + String[] functionValues = split[3].split(";"); + List convertedFunctionValues = new ArrayList<>(); + for (String functionValue : functionValues) { + convertedFunctionValues.add(new BigDecimal(functionValue)); + } + stocks.add(new Stock(symbol, company, price, convertedFunctionValues)); } catch (NumberFormatException e) { - throw new InvalidFormatException("Error with number conversion on line: " + l + "\n" + "Last field must be a number"); + throw new InvalidFormatException("Error with number conversion on line: " + l + "\n" + "ensure all number fields are actually numbers"); } catch (IllegalArgumentException e) { throw new InvalidFormatException("Illegal argument on line: " + l + "\n" + e.getMessage()); } diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index ef59694..f0ef821 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -10,6 +10,8 @@ import java.util.Map; import java.util.Random; import java.util.stream.Collectors; + +import millions.model.calculators.PriceChangeCalculator; import millions.model.factories.PurchaseFactory; import millions.model.factories.SaleFactory; import millions.model.factories.TransactionFactory; @@ -112,15 +114,19 @@ public List getLosers(int limit) { } public void advance() { + PriceChangeCalculator priceChangeCalculator = new PriceChangeCalculator(); this.weekNumber++; for (Stock stock : this.stocks.values()) { - double change = 0.9 + random.nextDouble() * 0.2; - stock.addNewSalesPrice( - stock - .getSalesPrice() - .multiply(BigDecimal.valueOf(change)) - .setScale(2, RoundingMode.HALF_UP)); - // RoundingMode from AI suggestion + BigDecimal change = priceChangeCalculator.calculateChange(stock); + stock.addNewSalesPrice(stock.getSalesPrice().add(change)); + + // double change = 0.9 + random.nextDouble() * 0.2; + // stock.addNewSalesPrice( + // stock + // .getSalesPrice() + // .multiply(BigDecimal.valueOf(change)) + // .setScale(2, RoundingMode.HALF_UP)); + // // RoundingMode from AI suggestion } notifyWeekAdvanced(); } diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index cc1a23f..95954e1 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -8,19 +8,27 @@ public class Stock { String symbol; String company; + List volatility; List prices; /** * @param symbol Stock ticker symbol * @param company company name * @param prices List of prices + * @param volatilityParameters numbers used for price change calculation functions * @throws IllegalArgumentException */ - public Stock(String symbol, String company, List prices) { + public Stock(String symbol, String company, List prices, List volatilityParameters) { this.symbol = symbol; this.company = company; this.prices = new ArrayList<>(prices); + this.volatility = volatilityParameters; + + if (volatilityParameters.size() != 5) { + throw new IllegalArgumentException("Invalid volatility function count"); + } + if (symbol == null || symbol.isBlank()) { throw new IllegalArgumentException("Symbol cannot be null or blank"); } @@ -31,8 +39,8 @@ public Stock(String symbol, String company, List prices) { } /** Stock() with single price instead of list */ - public Stock(String symbol, String company, BigDecimal initialPrice) { - this(symbol, company, new ArrayList<>(List.of(initialPrice))); + public Stock(String symbol, String company, BigDecimal initialPrice, List volatilityFunctions) { + this(symbol, company, new ArrayList<>(List.of(initialPrice)), volatilityFunctions); } /** @@ -110,6 +118,10 @@ public BigDecimal getLatestPriceChange() { return currentPrice.subtract(lastPrice); } + public List getVolatilityParameters() { + return this.volatility; + } + @Override public String toString() { return "Stock [symbol: " + symbol + ", company: " + company + ", prices: " + prices + "]"; diff --git a/src/main/java/millions/model/calculators/PriceChangeCalculator.java b/src/main/java/millions/model/calculators/PriceChangeCalculator.java new file mode 100644 index 0000000..c9b3074 --- /dev/null +++ b/src/main/java/millions/model/calculators/PriceChangeCalculator.java @@ -0,0 +1,56 @@ +package millions.model.calculators; + +import millions.model.Stock; + +import java.math.BigDecimal; +import java.util.List; + +public class PriceChangeCalculator { + + public PriceChangeCalculator() {} + + public BigDecimal calculateChange(Stock stock) { + BigDecimal change = BigDecimal.ZERO; + List volatilityParameters = stock.getVolatilityParameters(); + change = change.add(upChange(volatilityParameters.get(0))); + change = change.add(downChange(volatilityParameters.get(1))); + change = change.add(randomChange(volatilityParameters.get(2))); + change = change.add(sinChange(volatilityParameters.get(3))); + change = change.add(cosChange(volatilityParameters.get(4))); + return change; + } + + private BigDecimal upChange(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return input; + + } + private BigDecimal downChange(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return input.negate(); + } + + private BigDecimal randomChange(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return new BigDecimal(Math.random()*10).multiply(input); + } + + private BigDecimal sinChange(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return new BigDecimal(Math.sin(input.doubleValue())); + } + private BigDecimal cosChange(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return new BigDecimal(Math.cos(input.doubleValue())); + } +} From ff73c1baa3604479848fd53b6623402402eeaaf6 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 24 May 2026 15:00:22 +0200 Subject: [PATCH 71/83] Added logging for exceptions in startview. Continued JavaDoc documentation --- .../millions/controller/GameController.java | 35 +++++++++++++++++-- .../controller/fileIO/CSV/CSVFileHandler.java | 4 +-- .../fileIO/CSV/CSVStockFileParser.java | 2 +- .../UncheckedFileNotFoundException.java | 3 ++ src/main/java/millions/model/Exchange.java | 31 ++++++++++++++++ src/main/java/millions/model/Player.java | 18 +++++----- src/main/java/millions/model/Portfolio.java | 16 ++++----- src/main/java/millions/view/StartView.java | 7 +++- 8 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 7688f6f..a437790 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -63,7 +63,7 @@ public List getStocks() { * Gives alphabetic sort of findStocks * * @param searchTerm - * @return + * @return Alphabetically sorted list of stocks */ public List searchStocks(String searchTerm) { if (searchTerm == null || searchTerm.isBlank()) { @@ -78,12 +78,19 @@ public List searchStocks(String searchTerm) { * Get stocks with symbol * * @param symbol - * @return + * @return stocks with matching symbol */ public Stock getStock(String symbol) { return exchange.getStock(symbol); } + /** + * Purchases a stock + * + * @param symbol Symbol of desired stock + * @param quantity How much of a stock to purchase + * @return Transaction object for purchase + */ public Transaction buyStock(String symbol, BigDecimal quantity) { if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Quantity must be positive"); @@ -91,6 +98,12 @@ public Transaction buyStock(String symbol, BigDecimal quantity) { return exchange.buy(symbol, player, quantity); } + /** + * Sells a share + * + * @param share Share to sell + * @return Transaction object for sale + */ public Transaction sellShare(Share share) { if (share == null) { throw new IllegalArgumentException("Share cannot be null"); @@ -98,6 +111,12 @@ public Transaction sellShare(Share share) { return exchange.sell(share, player); } + /** + * Returns a players owned shares + * + * @param symbol Symbol for stock + * @return List of shares + */ public List getOwnedShares(String symbol) { return new ArrayList<>(player.getPortfolio().getShares(symbol)); } @@ -106,12 +125,24 @@ public void advanceWeek() { exchange.advance(); } + /** + * Returns the players owned quantity of a given share + * + * @param symbol Symbol for shares + * @return BigDecimal Quantity + */ public BigDecimal getOwnedQuantity(String symbol) { return player.getPortfolio().getShares(symbol).stream() .map(Share::getQuantity) .reduce(BigDecimal.ZERO, BigDecimal::add); } + /** + * Returns the maximum quantity of a stock the player can purchase + * + * @param symbol Symbol for stock + * @return int Quantity + */ public int getMaxBuyableQuantity(String symbol) { Stock stock = getStock(symbol); if (stock == null || player == null) { diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index fd2c3fb..95f7b56 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -10,7 +10,7 @@ import java.util.List; /** - *

Bundles StockFileReader and CSVStockFileParser together to reduce boilerplate code when reading stocks from a csv file

+ * Bundles StockFileReader and CSVStockFileParser together to reduce boilerplate code when reading stocks from a csv file */ public class CSVFileHandler { StockFileReader reader; @@ -24,7 +24,7 @@ public CSVFileHandler() { /** * Reads and parses stocks from a csv file * @param filePath Path to stock file - * @return list of stock object created from parsed file + * @return list of stock objects created from parsed file * @throws InvalidFormatException Throws an InvalidFormatException received from parser * @throws UncheckedFileNotFoundException Upon Receiving a FilenotFoundException */ diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 9208bbc..2a5d1ae 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -16,7 +16,7 @@ public CSVStockFileParser() {} * Verifies the amount of data fields present in supplied CSV file. * * @param lines Lines from csv file - * @return Boolean: True if file satisfies expected format (3 fields) + * @return Boolean: True if file satisfies expected format */ public boolean verifyCSV(List lines) { return lines.stream() diff --git a/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java b/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java index 0b4b02d..6f87d4c 100644 --- a/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java +++ b/src/main/java/millions/controller/fileIO/UncheckedFileNotFoundException.java @@ -1,5 +1,8 @@ package millions.controller.fileIO; +/** + * RuntimeException wrapper for FileNotFoundException. + */ public class UncheckedFileNotFoundException extends RuntimeException { public UncheckedFileNotFoundException(String message) { super(message); diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index ef59694..55b2ab7 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -41,6 +41,15 @@ public Exchange(String name, List stockList) { } } + /** + * Purchases a quantity of a given stock. + * + * @param symbol Symbol identifying the stock. + * @param player The player doing the purchase. + * @param quantity Quantity to purchase. + * @throws IllegalArgumentException If a stock isn't found + * @return Transaction object for purchase. + */ public Transaction buy(String symbol, Player player, BigDecimal quantity) { Stock stock = this.stocks.get(symbol); @@ -61,6 +70,13 @@ public Transaction buy(String symbol, Player player, int quantity) { return this.buy(symbol, player, BigDecimal.valueOf(quantity)); } + /** + * Creates a transaction for a sale. + * + * @param share share to sell. + * @param player player performing the sale. + * @return Transaction object for sale. + */ public Transaction sell(Share share, Player player) { Transaction sale = saleFactory.createTransaction(share, weekNumber); @@ -95,6 +111,12 @@ public List findStocks(String searchTerm) { .toList(); } + /** + * Returns the best performing stocks. + * + * @param limit Mmount of stocks to collect. + * @return Sorted list of stock objects sorted + */ public List getGainers(int limit) { Collection stocksCollection = stocks.values(); return stocksCollection.stream() @@ -103,6 +125,12 @@ public List getGainers(int limit) { .collect(Collectors.toList()); } + /** + * Returns the worst performing stocks. + * + * @param limit Amount of stocks to collect + * @return Sorted list of stock objects + */ public List getLosers(int limit) { Collection stocksCollection = stocks.values(); return stocksCollection.stream() @@ -111,6 +139,9 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } + /** + * Advances the current game week by performing new price calculations for all stocks. + */ public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 2dfd684..e4abeae 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -62,10 +62,10 @@ public void withdrawMoney(BigDecimal amount) { } /** - * @return + * Calculates the skill level of the player + * @return String player status level */ public String getStatus() { - // TODO dobbel sjekk logikken int weeksTraded = transactionArchive.countDistinctWeeks(); String status = "Novice"; @@ -81,21 +81,21 @@ public String getStatus() { } /** - * @return + * @return player name */ public String getName() { return this.name; } /** - * @return + * @return player money */ public BigDecimal getMoney() { return this.money; } /** - * @return + * @return player portfolio */ public Portfolio getPortfolio() { return this.portfolio; @@ -120,28 +120,28 @@ public void removeShareFromPortfolio(Share share) { } /** - * @return + * @return player net worth */ public BigDecimal getNetWorth() { return this.money.add(this.portfolio.getNetWorth()); } /** - * @return + * @return TransactionArchive object */ public TransactionArchive getTransactionArchive() { return this.transactionArchive; } /** - * @param listener + * @param listener PlayerListener */ public void addListener(PlayerListener listener) { listeners.add(listener); } /** - * @param listener + * @param listener PlayerListener */ public void removeListener(PlayerListener listener) { listeners.remove(listener); diff --git a/src/main/java/millions/model/Portfolio.java b/src/main/java/millions/model/Portfolio.java index 210f784..4374595 100644 --- a/src/main/java/millions/model/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -15,7 +15,7 @@ public Portfolio() { /** * @param share Share to be added - * @return + * @return Boolean for success */ public boolean addShare(Share share) { return this.shares.add(share); @@ -23,22 +23,22 @@ public boolean addShare(Share share) { /** * @param share Share to be removed - * @return + * @return Boolean for success */ public boolean removeShare(Share share) { return this.shares.remove(share); } /** - * @return + * @return List of shares */ public List getShares() { return this.shares; } /** - * @param symbol - * @return + * @param symbol Symbol for share + * @return List of shares */ public List getShares(String symbol) { return this.shares.stream() @@ -47,7 +47,7 @@ public List getShares(String symbol) { } /** - * @return + * @return BigDecimal net worth */ public BigDecimal getNetWorth() { BigDecimal total = BigDecimal.ZERO; @@ -59,8 +59,8 @@ public BigDecimal getNetWorth() { } /** - * @param share - * @return + * @param share Share + * @return Boolean */ public boolean contains(Share share) { return this.shares.contains(share); diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 44e02f6..dd7dfba 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -6,6 +6,9 @@ import java.net.URL; import java.nio.file.Paths; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,10 +18,11 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; +import millions.App; /** The initial game setup screen where the player enters their info. */ public class StartView extends VBox { - + private static final Logger logger = Logger.getLogger(StartView.class.getName()); private TextField nameField; private TextField startingAmountField; private TextField preRunWeeksField; @@ -110,6 +114,7 @@ private File loadDefaultStocksFile() { URL resource = Objects.requireNonNull(getClass().getResource("/data/default-stocks.csv")); return Paths.get(resource.toURI()).toFile(); } catch (URISyntaxException e) { + logger.log(Level.SEVERE, "Error accessing default stocks file", e); throw new IllegalStateException("Could not load default stocks file", e); } } From 3a96abbf4f36a72b3df4a502ede11348c78aaf85 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 24 May 2026 15:04:29 +0200 Subject: [PATCH 72/83] Refactored test packagestructure to match project structure --- .../fileIO/CSV}/CSVStockFileParserTest.java | 4 +--- .../fileIO/CSV}/StockFileReaderTest.java | 2 +- .../millions/{ => model}/ExchangeListenerTest.java | 11 ++--------- src/test/java/millions/{ => model}/ExchangeTest.java | 4 ++-- .../java/millions/{ => model}/PlayerListenerTest.java | 4 ++-- src/test/java/millions/{ => model}/PlayerTest.java | 4 ++-- src/test/java/millions/{ => model}/PortfolioTest.java | 6 ++---- src/test/java/millions/{ => model}/PurchaseTest.java | 6 +----- src/test/java/millions/{ => model}/SaleTest.java | 6 +----- src/test/java/millions/{ => model}/ShareTest.java | 4 +--- src/test/java/millions/{ => model}/StockTest.java | 4 ++-- .../millions/{ => model}/TransactionArchiveTest.java | 3 +-- 12 files changed, 18 insertions(+), 40 deletions(-) rename src/test/java/millions/{ => controller/fileIO/CSV}/CSVStockFileParserTest.java (96%) rename src/test/java/millions/{ => controller/fileIO/CSV}/StockFileReaderTest.java (96%) rename src/test/java/millions/{ => model}/ExchangeListenerTest.java (90%) rename src/test/java/millions/{ => model}/ExchangeTest.java (99%) rename src/test/java/millions/{ => model}/PlayerListenerTest.java (98%) rename src/test/java/millions/{ => model}/PlayerTest.java (97%) rename src/test/java/millions/{ => model}/PortfolioTest.java (95%) rename src/test/java/millions/{ => model}/PurchaseTest.java (90%) rename src/test/java/millions/{ => model}/SaleTest.java (88%) rename src/test/java/millions/{ => model}/ShareTest.java (93%) rename src/test/java/millions/{ => model}/StockTest.java (97%) rename src/test/java/millions/{ => model}/TransactionArchiveTest.java (96%) diff --git a/src/test/java/millions/CSVStockFileParserTest.java b/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java similarity index 96% rename from src/test/java/millions/CSVStockFileParserTest.java rename to src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java index 8266d35..512cfc9 100644 --- a/src/test/java/millions/CSVStockFileParserTest.java +++ b/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java @@ -1,12 +1,10 @@ -package millions; +package millions.controller.fileIO.CSV; -import millions.controller.fileIO.CSV.CSVStockFileParser; import millions.controller.fileIO.InvalidFormatException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.List; diff --git a/src/test/java/millions/StockFileReaderTest.java b/src/test/java/millions/controller/fileIO/CSV/StockFileReaderTest.java similarity index 96% rename from src/test/java/millions/StockFileReaderTest.java rename to src/test/java/millions/controller/fileIO/CSV/StockFileReaderTest.java index 2bc8970..159c948 100644 --- a/src/test/java/millions/StockFileReaderTest.java +++ b/src/test/java/millions/controller/fileIO/CSV/StockFileReaderTest.java @@ -1,4 +1,4 @@ -package millions; +package millions.controller.fileIO.CSV; import millions.controller.fileIO.StockFileReader; import org.junit.jupiter.api.BeforeAll; diff --git a/src/test/java/millions/ExchangeListenerTest.java b/src/test/java/millions/model/ExchangeListenerTest.java similarity index 90% rename from src/test/java/millions/ExchangeListenerTest.java rename to src/test/java/millions/model/ExchangeListenerTest.java index c47c9e8..935ab43 100644 --- a/src/test/java/millions/ExchangeListenerTest.java +++ b/src/test/java/millions/model/ExchangeListenerTest.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -6,14 +6,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import millions.model.Exchange; -import millions.model.ExchangeListener; -import millions.model.Player; -import millions.model.Purchase; -import millions.model.Sale; -import millions.model.Share; -import millions.model.Stock; -import millions.model.Transaction; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/model/ExchangeTest.java similarity index 99% rename from src/test/java/millions/ExchangeTest.java rename to src/test/java/millions/model/ExchangeTest.java index a17bb50..2e689a6 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/model/ExchangeTest.java @@ -1,11 +1,11 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; import java.util.List; import java.util.stream.IntStream; -import millions.model.*; + import org.junit.jupiter.api.Test; class ExchangeTest { diff --git a/src/test/java/millions/PlayerListenerTest.java b/src/test/java/millions/model/PlayerListenerTest.java similarity index 98% rename from src/test/java/millions/PlayerListenerTest.java rename to src/test/java/millions/model/PlayerListenerTest.java index 5e2008a..71c94d6 100644 --- a/src/test/java/millions/PlayerListenerTest.java +++ b/src/test/java/millions/model/PlayerListenerTest.java @@ -1,11 +1,11 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import millions.model.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/model/PlayerTest.java similarity index 97% rename from src/test/java/millions/PlayerTest.java rename to src/test/java/millions/model/PlayerTest.java index 011a6fb..2cefd2d 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/model/PlayerTest.java @@ -1,9 +1,9 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import millions.model.Player; + import org.junit.jupiter.api.Test; class PlayerTest { diff --git a/src/test/java/millions/PortfolioTest.java b/src/test/java/millions/model/PortfolioTest.java similarity index 95% rename from src/test/java/millions/PortfolioTest.java rename to src/test/java/millions/model/PortfolioTest.java index b151f94..a208066 100644 --- a/src/test/java/millions/PortfolioTest.java +++ b/src/test/java/millions/model/PortfolioTest.java @@ -1,11 +1,9 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import millions.model.Portfolio; -import millions.model.Share; -import millions.model.Stock; + import org.junit.jupiter.api.Test; class PortfolioTest { diff --git a/src/test/java/millions/PurchaseTest.java b/src/test/java/millions/model/PurchaseTest.java similarity index 90% rename from src/test/java/millions/PurchaseTest.java rename to src/test/java/millions/model/PurchaseTest.java index 941ac87..3c76e85 100644 --- a/src/test/java/millions/PurchaseTest.java +++ b/src/test/java/millions/model/PurchaseTest.java @@ -1,13 +1,9 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import millions.model.Player; -import millions.model.Purchase; -import millions.model.Share; -import millions.model.Stock; import org.junit.jupiter.api.Test; class PurchaseTest { diff --git a/src/test/java/millions/SaleTest.java b/src/test/java/millions/model/SaleTest.java similarity index 88% rename from src/test/java/millions/SaleTest.java rename to src/test/java/millions/model/SaleTest.java index 2d725f4..4a8a4d1 100644 --- a/src/test/java/millions/SaleTest.java +++ b/src/test/java/millions/model/SaleTest.java @@ -1,13 +1,9 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import millions.model.Player; -import millions.model.Sale; -import millions.model.Share; -import millions.model.Stock; import org.junit.jupiter.api.Test; class SaleTest { diff --git a/src/test/java/millions/ShareTest.java b/src/test/java/millions/model/ShareTest.java similarity index 93% rename from src/test/java/millions/ShareTest.java rename to src/test/java/millions/model/ShareTest.java index 25862ee..3f82181 100644 --- a/src/test/java/millions/ShareTest.java +++ b/src/test/java/millions/model/ShareTest.java @@ -1,11 +1,9 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import millions.model.Share; -import millions.model.Stock; import org.junit.jupiter.api.Test; class ShareTest { diff --git a/src/test/java/millions/StockTest.java b/src/test/java/millions/model/StockTest.java similarity index 97% rename from src/test/java/millions/StockTest.java rename to src/test/java/millions/model/StockTest.java index 452db31..c28a6bd 100644 --- a/src/test/java/millions/StockTest.java +++ b/src/test/java/millions/model/StockTest.java @@ -1,4 +1,4 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import millions.model.Stock; + import org.junit.jupiter.api.Test; class StockTest { diff --git a/src/test/java/millions/TransactionArchiveTest.java b/src/test/java/millions/model/TransactionArchiveTest.java similarity index 96% rename from src/test/java/millions/TransactionArchiveTest.java rename to src/test/java/millions/model/TransactionArchiveTest.java index c87ef93..426ed5b 100644 --- a/src/test/java/millions/TransactionArchiveTest.java +++ b/src/test/java/millions/model/TransactionArchiveTest.java @@ -1,11 +1,10 @@ -package millions; +package millions.model; import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; import java.util.List; -import millions.model.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From 1e156e1e8ce7f47a06801dccda29294d050f15e1 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Sun, 24 May 2026 16:32:03 +0200 Subject: [PATCH 73/83] Changed where FileNotFoundException is wrapped in an UncheckedFileNotFoundException when reading from file --- src/main/java/millions/controller/GameController.java | 3 ++- .../millions/controller/fileIO/CSV/CSVFileHandler.java | 7 ++++--- .../java/millions/controller/fileIO/StockFileReader.java | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index a437790..17157dc 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -1,5 +1,6 @@ package millions.controller; +import java.io.FileNotFoundException; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.file.Path; @@ -38,7 +39,7 @@ public void startGame( player = new Player(name, startingMoney); - } catch (UncheckedFileNotFoundException e) { + } catch (FileNotFoundException e) { throw new UncheckedFileNotFoundException(e.getMessage()); } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index 95f7b56..051e1f0 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -6,6 +6,7 @@ import millions.model.Stock; +import java.io.FileNotFoundException; import java.nio.file.Path; import java.util.List; @@ -28,7 +29,7 @@ public CSVFileHandler() { * @throws InvalidFormatException Throws an InvalidFormatException received from parser * @throws UncheckedFileNotFoundException Upon Receiving a FilenotFoundException */ - public List getStocksFromFile(Path filePath) { + public List getStocksFromFile(Path filePath) throws FileNotFoundException { try { StockFileReader reader = new StockFileReader(); List lines = reader.readFile(filePath); @@ -38,8 +39,8 @@ public List getStocksFromFile(Path filePath) { } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); - } catch (UncheckedFileNotFoundException e) { - throw new UncheckedFileNotFoundException(e.getMessage()); + } catch (FileNotFoundException e) { + throw new FileNotFoundException(e.getMessage()); } } } diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index f7121cf..6c8069d 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -23,7 +23,7 @@ public StockFileReader() {} * @return List of each line in the file as a string * @throws UncheckedFileNotFoundException Upon encountering a FileNotFoundException */ - public List readFile(Path path) { + public List readFile(Path path) throws FileNotFoundException { File file = new File(path.toString()); List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); @@ -34,7 +34,7 @@ public List readFile(Path path) { } } catch (IOException e) { if (e instanceof FileNotFoundException) { - throw new UncheckedFileNotFoundException("Couldn't find file at specified path"); + throw new FileNotFoundException("Couldn't find file at specified path"); } else { logger.log(Level.SEVERE, "Encountered unexpected IOException: ", e.getMessage()); From 9d4abda84b337decd4806ba37ce07b31da5f4296 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 25 May 2026 08:32:33 +0200 Subject: [PATCH 74/83] feat: Adding transactions table with colors --- src/main/java/millions/view/GameView.java | 84 +++++++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 64acd71..32b03ce 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.util.List; +import javafx.beans.property.SimpleStringProperty; // For data binding for table string import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -13,6 +14,9 @@ import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; @@ -39,8 +43,8 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); - private final ListView portfolioList = new ListView<>(); - private final ListView transactionsList = new ListView<>(); + private final TableView portfolioTable = new TableView<>(); + private final TableView transactionsTable = new TableView<>(); private final Label selectedStockLabel = new Label("Select a stock to see chart"); private final Label ownedQuantityLabel = new Label("Owned: 0"); private final Label actionStatusLabel = new Label(); @@ -127,13 +131,81 @@ private Tab createStocksTab() { } private Tab createPortfolioTab() { - portfolioList.setPlaceholder(new Label("No shares yet")); - return new Tab("Portfolio", portfolioList); + portfolioTable.setPlaceholder(new Label("No shares yet")); + + TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getStock().getSymbol())); + + TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getQuantity().toPlainString())); + + TableColumn purchasePriceColumn = new TableColumn<>("Purchase price"); + purchasePriceColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getPurchasePrice().toPlainString())); + + TableColumn currentPriceColumn = new TableColumn<>("Current price"); + currentPriceColumn.setCellValueFactory( + data -> + new SimpleStringProperty(data.getValue().getStock().getSalesPrice().toPlainString())); + + TableColumn profitColumn = new TableColumn<>("Profit"); + profitColumn.setCellValueFactory( + data -> + new SimpleStringProperty(ViewUtils.getShareProfit(data.getValue()).toPlainString())); + profitColumn.setCellFactory( + column -> + new TableCell<>() { + @Override + protected void updateItem(String profit, boolean empty) { + super.updateItem(profit, empty); + if (empty) { + setText(null); + setStyle(""); + return; + } + setText(profit); + setStyle(profit.startsWith("-") ? "-fx-text-fill: red;" : "-fx-text-fill: green;"); + } + }); + + portfolioTable + .getColumns() + .addAll( + symbolColumn, quantityColumn, purchasePriceColumn, currentPriceColumn, profitColumn); + return new Tab("Portfolio", portfolioTable); } private Tab createTransactionsTab() { - transactionsList.setPlaceholder(new Label("No transactions yet")); - return new Tab("Transactions", transactionsList); + transactionsTable.setPlaceholder(new Label("No transactions yet")); + + TableColumn typeColumn = new TableColumn<>("Type"); + typeColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getClass().getSimpleName())); + + TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getShare().getStock().getSymbol())); + + TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getShare().getQuantity().toPlainString())); + + TableColumn weekColumn = new TableColumn<>("Week"); + weekColumn.setCellValueFactory( + data -> new SimpleStringProperty(String.valueOf(data.getValue().getWeek()))); + + TableColumn totalColumn = new TableColumn<>("Total"); + totalColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + data.getValue().getCalculator().calculateTotal().toPlainString())); + + transactionsTable + .getColumns() + .addAll(typeColumn, symbolColumn, quantityColumn, weekColumn, totalColumn); + return new Tab("Transactions", transactionsTable); } private void configureButtons() { From 69f7abe23e5ba7a53944c0efdd33d305a5cbf8a0 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 25 May 2026 10:34:08 +0200 Subject: [PATCH 75/83] Merging changes, defined price functions --- .../fileIO/CSV/CSVStockFileParser.java | 4 +- src/main/java/millions/model/Exchange.java | 29 +++++----- src/main/java/millions/model/Stock.java | 2 +- .../calculators/PriceChangeCalculator.java | 54 +++++++++++-------- src/main/java/millions/view/GameView.java | 14 +---- 5 files changed, 50 insertions(+), 53 deletions(-) diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 04e80db..caf1097 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -21,14 +21,14 @@ public CSVStockFileParser() {} public boolean verifyCSV(List lines) { return lines.stream() .filter(l -> !(l.startsWith("#") || l.isBlank())) - .noneMatch(l -> l.split(",").length != 4); + .noneMatch(l -> l.split(",").length != 4 || l.split(",")[3].split(";").length != 6); } /** * Parses the supplied lines if they satisfy the correct format expectations * * @param lines

lines to be parsed.
- * Each line must contain three data fields: String,String,BigDecimal
+ * Each line must contain four data fields: String,String,BigDecimal,String
* (Fields cannot be blank)
* blank lines or lines beginning with '#' are ignored *

diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index f96d2bb..fbbec8d 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -10,14 +10,14 @@ import java.util.Map; import java.util.Random; import java.util.stream.Collectors; - import millions.model.calculators.PriceChangeCalculator; import millions.model.factories.PurchaseFactory; import millions.model.factories.SaleFactory; import millions.model.factories.TransactionFactory; /** - * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price changes. + * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price + * changes. */ public class Exchange { private String name; @@ -141,23 +141,22 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } - /** - * Advances the current game week by performing new price calculations for all stocks. - */ + /** Advances the current game week by performing new price calculations for all stocks. */ public void advance() { - PriceChangeCalculator priceChangeCalculator = new PriceChangeCalculator(); + PriceChangeCalculator priceChangeCalculator = new PriceChangeCalculator(); this.weekNumber++; for (Stock stock : this.stocks.values()) { BigDecimal change = priceChangeCalculator.calculateChange(stock); - stock.addNewSalesPrice(stock.getSalesPrice().add(change)); - - // double change = 0.9 + random.nextDouble() * 0.2; - // stock.addNewSalesPrice( - // stock - // .getSalesPrice() - // .multiply(BigDecimal.valueOf(change)) - // .setScale(2, RoundingMode.HALF_UP)); - // // RoundingMode from AI suggestion + stock.addNewSalesPrice(stock.getSalesPrice().add(change).setScale(2, RoundingMode.HALF_UP)); + // Round to stop crazy values + + // double change = 0.9 + random.nextDouble() * 0.2; + // stock.addNewSalesPrice( + // stock + // .getSalesPrice() + // .multiply(BigDecimal.valueOf(change)) + // .setScale(2, RoundingMode.HALF_UP)); + // // RoundingMode from AI suggestion } notifyWeekAdvanced(); } diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 95954e1..07dad71 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -25,7 +25,7 @@ public Stock(String symbol, String company, List prices, List values = stock.getVolatilityParameters(); + int week = stock.getHistoricalPrices().size(); BigDecimal change = BigDecimal.ZERO; - List volatilityParameters = stock.getVolatilityParameters(); - change = change.add(upChange(volatilityParameters.get(0))); - change = change.add(downChange(volatilityParameters.get(1))); - change = change.add(randomChange(volatilityParameters.get(2))); - change = change.add(sinChange(volatilityParameters.get(3))); - change = change.add(cosChange(volatilityParameters.get(4))); - return change; + change = change.add(drift(values.get(0))); + change = change.add(volatility(values.get(1))); + change = change.add(cycle(values.get(2), values.get(3), week)); + change = change.add(explosion(values.get(4), values.get(5))); + return stock.getSalesPrice().multiply(change); } - private BigDecimal upChange(BigDecimal input) { + /* + * Flat slope change, eg upwards/downwards slope + */ + private BigDecimal drift(BigDecimal input) { if (input.equals(BigDecimal.ZERO)) { return BigDecimal.ZERO; } return input; - } - private BigDecimal downChange(BigDecimal input) { + + /* + * Random noise of size x + */ + private BigDecimal volatility(BigDecimal input) { if (input.equals(BigDecimal.ZERO)) { return BigDecimal.ZERO; } - return input.negate(); + return input.multiply(BigDecimal.valueOf(Math.random() * 2 - 1)); } - private BigDecimal randomChange(BigDecimal input) { - if (input.equals(BigDecimal.ZERO)) { + /* + * Sinus curve based on week, times size of the curve + */ + private BigDecimal cycle(BigDecimal speed, BigDecimal size, int week) { + if (speed.equals(BigDecimal.ZERO) || size.equals(BigDecimal.ZERO)) { return BigDecimal.ZERO; } - return new BigDecimal(Math.random()*10).multiply(input); + return size.multiply(BigDecimal.valueOf(Math.sin(week * speed.doubleValue()))); } - private BigDecimal sinChange(BigDecimal input) { - if (input.equals(BigDecimal.ZERO)) { + /* + * probability% change of an explosion of size% positive or negative + */ + private BigDecimal explosion(BigDecimal probability, BigDecimal size) { + if (probability.equals(BigDecimal.ZERO) || size.equals(BigDecimal.ZERO)) { return BigDecimal.ZERO; } - return new BigDecimal(Math.sin(input.doubleValue())); - } - private BigDecimal cosChange(BigDecimal input) { - if (input.equals(BigDecimal.ZERO)) { + if (Math.random() >= probability.doubleValue()) { return BigDecimal.ZERO; } - return new BigDecimal(Math.cos(input.doubleValue())); + return Math.random() < 0.5 ? size : size.negate(); } } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 32b03ce..ad63e4e 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -338,12 +338,7 @@ private void refreshQuantityControls(Stock stock) { private void refreshPortfolio() { Player player = controller.getPlayer(); - portfolioList - .getItems() - .setAll( - player.getPortfolio().getShares().stream() - .map(ViewUtils::formatPortfolioShare) - .toList()); + portfolioTable.getItems().setAll(player.getPortfolio().getShares()); } private void refreshTransactions() { @@ -352,12 +347,7 @@ private void refreshTransactions() { return; } - transactionsList - .getItems() - .setAll( - player.getTransactionArchive().getTransactions().stream() - .map(ViewUtils::formatTransaction) - .toList()); + transactionsTable.getItems().setAll(player.getTransactionArchive().getTransactions()); } private void buySelectedStock() { From 4079e92cac47d35f481e5af27334326ad8fbc650 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 25 May 2026 13:04:09 +0200 Subject: [PATCH 76/83] Deleted unused duplicate files, continued javadoc documentation --- .../TransactionCalculatorFactory.java | 22 --------------- src/main/java/millions/model/Share.java | 16 ++++++----- src/main/java/millions/model/Stock.java | 18 ++++++------- src/main/java/millions/model/Transaction.java | 12 +++++++++ .../millions/model/TransactionArchive.java | 27 ++++++++++++++++++- 5 files changed, 56 insertions(+), 39 deletions(-) delete mode 100644 src/main/java/millions/calculators/TransactionCalculatorFactory.java diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java deleted file mode 100644 index 36a71c0..0000000 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ /dev/null @@ -1,22 +0,0 @@ -package millions.calculators; - -import millions.model.Share; -import millions.model.calculators.PurchaseCalculator; -import millions.model.calculators.SaleCalculator; -import millions.model.calculators.TransactionCalculator; - -/** - * Factory for creating transaction calculators. - */ -public class TransactionCalculatorFactory { - - private TransactionCalculatorFactory() {} - - public TransactionCalculator createPurchaseCalculator(Share share) { - return new PurchaseCalculator(share); - } - - public TransactionCalculator createSaleCalculator(Share share) { - return new SaleCalculator(share); - } -} diff --git a/src/main/java/millions/model/Share.java b/src/main/java/millions/model/Share.java index 7967b95..5a7a726 100644 --- a/src/main/java/millions/model/Share.java +++ b/src/main/java/millions/model/Share.java @@ -10,9 +10,11 @@ public class Share { /** * @param stock Which stock the share is for. - * @param quantity How many stocks - * @param purchasePrice Purchase price of the share - * @throws IllegalArgumentException + * @param quantity How many stocks. + * @param purchasePrice Purchase price of the share. + * @throws IllegalArgumentException if stock is null. + * @throws IllegalArgumentException if quantity is null. + * @throws IllegalArgumentException if purchasePrice is null. */ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.stock = stock; @@ -30,27 +32,27 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { } } - /** Share() with int quantity */ + /** Share() with int quantity. */ public Share(Stock stock, int quantity, BigDecimal purchasePrice) { this(stock, BigDecimal.valueOf(quantity), purchasePrice); } /** - * @return + * @return Stock object. */ public Stock getStock() { return this.stock; } /** - * @return + * @return BigDecimal: quantity. */ public BigDecimal getQuantity() { return this.quantity; } /** - * @return + * @return BigDecimal PurchasePrice. */ public BigDecimal getPurchasePrice() { return this.purchasePrice; diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index cc1a23f..e21fb9e 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -30,48 +30,48 @@ public Stock(String symbol, String company, List prices) { } } - /** Stock() with single price instead of list */ + /** Stock() with single price instead of list. */ public Stock(String symbol, String company, BigDecimal initialPrice) { this(symbol, company, new ArrayList<>(List.of(initialPrice))); } /** - * @return + * @return String: symbol. */ public String getSymbol() { return this.symbol; } /** - * @return + * @return String: company. */ public String getCompany() { return this.company; } /** - * @return + * @return BigDecimal: price. */ public BigDecimal getSalesPrice() { return this.prices.getLast(); } /** - * @param price Sales price + * @param price Sales price. */ public void addNewSalesPrice(BigDecimal price) { this.prices.add(price); } /** - * @return + * @return BigDecimal list of prices. */ public List getHistoricalPrices() { return this.prices; } /** - * @return + * @return BigDecimal highest recorded price. */ public BigDecimal getHighestPrice() { BigDecimal highestPrice = this.prices.get(0); @@ -84,7 +84,7 @@ public BigDecimal getHighestPrice() { } /** - * @return + * @return BigDecimal lowest recorded price. */ public BigDecimal getLowestPrice() { BigDecimal lowestPrice = this.prices.get(0); @@ -97,7 +97,7 @@ public BigDecimal getLowestPrice() { } /** - * @return + * @return BigDecimal price difference from last week */ public BigDecimal getLatestPriceChange() { if (this.prices.size() < 2) { diff --git a/src/main/java/millions/model/Transaction.java b/src/main/java/millions/model/Transaction.java index 6e1e8b1..69c1167 100644 --- a/src/main/java/millions/model/Transaction.java +++ b/src/main/java/millions/model/Transaction.java @@ -17,18 +17,30 @@ protected Transaction(Share share, int week, TransactionCalculator transactionCa this.committed = false; } + /** + * @return Share object + */ public Share getShare() { return this.share; } + /** + * @return int: week + */ public int getWeek() { return this.week; } + /** + * @return TransactionCalculator object + */ public TransactionCalculator getCalculator() { return this.transactionCalculator; } + /** + * @return Boolean: status + */ public boolean isCommitted() { return this.committed; } diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 6910a6c..20b7e7a 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -13,6 +13,11 @@ public TransactionArchive() { this.transactions = new ArrayList<>(); } + /** + * Adds a transaction to the archive + * @param transaction transaction object + * @return Boolean for success + */ public boolean add(Transaction transaction) { if (transactions.contains(transaction)) { return false; @@ -21,25 +26,45 @@ public boolean add(Transaction transaction) { return true; } + /** + * @return Boolean + */ public boolean isEmpty() { return transactions.isEmpty(); } + /** + * @return List of transaction objects + */ public List getTransactions() { return new ArrayList<>(transactions); } + /** + * returns transaction processed in a given week + * @param week int + * @return List of transaction objects + */ public List getTransactions(int week) { return transactions.stream().filter(x -> x.getWeek() == week).collect(Collectors.toList()); } + /** + * Returns all purchase transactions in a given week + * @param week int + * @return List of transaction objects + */ public List getPurchases(int week) { return transactions.stream() .filter(t -> t.getWeek() == week && t instanceof Purchase) .map(t -> (Purchase) t) .collect(Collectors.toList()); } - + /** + * Returns all sale transactions in a given week + * @param week int + * @return List of transaction objects + */ public List getSales(int week) { return transactions.stream() .filter(t -> t.getWeek() == week && t instanceof Sale) From 18a74af007fd9b6cb124a4fa7de87cccdbdd3f61 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 25 May 2026 13:14:17 +0200 Subject: [PATCH 77/83] Fixed CSVStockFileParserTest, moved calculatortests to model to match project structure --- .../controller/fileIO/CSV/CSVStockFileParserTest.java | 5 +++-- .../{ => model}/calculators/PurchaseCalculatorTest.java | 2 +- .../millions/{ => model}/calculators/SaleCalculatorTest.java | 2 +- .../{ => model}/calculators/TransactionCalculatorTest.java | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) rename src/test/java/millions/{ => model}/calculators/PurchaseCalculatorTest.java (70%) rename src/test/java/millions/{ => model}/calculators/SaleCalculatorTest.java (68%) rename src/test/java/millions/{ => model}/calculators/TransactionCalculatorTest.java (70%) diff --git a/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java b/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java index 512cfc9..efefb7d 100644 --- a/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java +++ b/src/test/java/millions/controller/fileIO/CSV/CSVStockFileParserTest.java @@ -2,6 +2,7 @@ import millions.controller.fileIO.InvalidFormatException; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,8 +17,8 @@ public class CSVStockFileParserTest { static String exampleString; final CSVStockFileParser parser = new CSVStockFileParser(); - @BeforeAll - public static void setUpTestString() { + @BeforeEach + public void setUpTestString() { exampleString = "# Top 500 US Stocks by Market Cap\n"; exampleString += "# Ticker,Name,Price\n"; exampleString += "\n"; diff --git a/src/test/java/millions/calculators/PurchaseCalculatorTest.java b/src/test/java/millions/model/calculators/PurchaseCalculatorTest.java similarity index 70% rename from src/test/java/millions/calculators/PurchaseCalculatorTest.java rename to src/test/java/millions/model/calculators/PurchaseCalculatorTest.java index 2915b5e..37d8cca 100644 --- a/src/test/java/millions/calculators/PurchaseCalculatorTest.java +++ b/src/test/java/millions/model/calculators/PurchaseCalculatorTest.java @@ -1,4 +1,4 @@ -package millions.calculators; +package millions.model.calculators; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/millions/calculators/SaleCalculatorTest.java b/src/test/java/millions/model/calculators/SaleCalculatorTest.java similarity index 68% rename from src/test/java/millions/calculators/SaleCalculatorTest.java rename to src/test/java/millions/model/calculators/SaleCalculatorTest.java index 3d7cebb..ea19922 100644 --- a/src/test/java/millions/calculators/SaleCalculatorTest.java +++ b/src/test/java/millions/model/calculators/SaleCalculatorTest.java @@ -1,4 +1,4 @@ -package millions.calculators; +package millions.model.calculators; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/millions/calculators/TransactionCalculatorTest.java b/src/test/java/millions/model/calculators/TransactionCalculatorTest.java similarity index 70% rename from src/test/java/millions/calculators/TransactionCalculatorTest.java rename to src/test/java/millions/model/calculators/TransactionCalculatorTest.java index ad398f8..b65dc0e 100644 --- a/src/test/java/millions/calculators/TransactionCalculatorTest.java +++ b/src/test/java/millions/model/calculators/TransactionCalculatorTest.java @@ -1,4 +1,4 @@ -package millions.calculators; +package millions.model.calculators; import static org.junit.jupiter.api.Assertions.*; From 9d0cbfd112f98f60754271a074760b79f1e96160 Mon Sep 17 00:00:00 2001 From: Nikolai Oliver Aasheim Lydvo Date: Mon, 25 May 2026 14:26:39 +0200 Subject: [PATCH 78/83] Create README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c8724c --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +Millions stock trading game + +Made by: +- Nikolai Oliver Aasheim Lydvo +- Martin Olai Amundsen Henøen + +How to run the game: +1. Download the latest release +2. Ensure you have Java 25 and the Maven commandline tool installed +3. Navigate to the project root directory and run the command: "mvn javafx:run" +4. Set up your game configuration and start game From d3a095f8a4b10561f21a6c2c8f0d85d988b85c25 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 25 May 2026 15:20:38 +0200 Subject: [PATCH 79/83] Added a button to redirect to stock market page from portfolio tab Added GameControllerTest --- src/main/java/millions/view/GameView.java | 34 +++++++++++++++++-- .../controller/GameControllerTest.java | 27 +++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/test/java/millions/controller/GameControllerTest.java diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index ad63e4e..4dc86f2 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -57,12 +57,14 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Button buyButton = new Button("Buy"); private final Button sellButton = new Button("Sell"); private final Button advanceButton = new Button("Advance week"); + private final TabPane tabPane; private boolean updatingQuantityControls; public GameView(GameController controller) { this.controller = controller; + this.tabPane = createTabs(); setTop(createHeader()); - setCenter(createTabs()); + setCenter(tabPane); configureStocksList(); configureButtons(); configureQuantityControls(); @@ -169,11 +171,34 @@ protected void updateItem(String profit, boolean empty) { setStyle(profit.startsWith("-") ? "-fx-text-fill: red;" : "-fx-text-fill: green;"); } }); - + TableColumn marketColumn = new TableColumn<>("Market"); + marketColumn.setMinWidth(100); + marketColumn.setCellFactory(column -> { + return new TableCell() { + private final Button button = new Button("Market Page"); + + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + + if (empty) { + setGraphic(null); + } else { + button.setOnAction(event -> { + Share s = getTableView().getItems().get(getIndex()); + getTabPane().getSelectionModel().select(0); + stocksList.getSelectionModel().select(s.getStock()); + showStockChart(s.getStock()); + }); + setGraphic(button); + } + } + }; + }); portfolioTable .getColumns() .addAll( - symbolColumn, quantityColumn, purchasePriceColumn, currentPriceColumn, profitColumn); + symbolColumn, quantityColumn, purchasePriceColumn, currentPriceColumn, profitColumn, marketColumn); return new Tab("Portfolio", portfolioTable); } @@ -457,6 +482,9 @@ private int getCurrentMaxBuyable() { return Math.max(1, controller.getMaxBuyableQuantity(selected.getSymbol())); } + public TabPane getTabPane() { + return this.tabPane; + } private String formatStock(Stock stock) { return ViewUtils.formatStock(stock); } diff --git a/src/test/java/millions/controller/GameControllerTest.java b/src/test/java/millions/controller/GameControllerTest.java new file mode 100644 index 0000000..fd63cc6 --- /dev/null +++ b/src/test/java/millions/controller/GameControllerTest.java @@ -0,0 +1,27 @@ +package millions.controller; + +import millions.controller.fileIO.UncheckedFileNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class GameControllerTest { + GameController gameController; + @BeforeEach + public void setUpController() { + this.gameController = new GameController(); + } + + @Test + public void startGameFileNotFoundTest() { + String name = "name"; + BigDecimal startingMoney = new BigDecimal("100"); + Path stockFilePath = Path.of("nonexistantfile"); + int prerunWeeks = 1; + assertThrows(UncheckedFileNotFoundException.class, () -> {gameController.startGame(name,startingMoney,stockFilePath,prerunWeeks);}); + } +} From 381a64d971dabf70206c0cc7e76f5219b408f2b7 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Mon, 25 May 2026 15:34:09 +0200 Subject: [PATCH 80/83] Small UI tweaks --- src/main/java/millions/view/GameView.java | 39 +++++++++++----------- src/main/java/millions/view/StartView.java | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 4dc86f2..d2339ff 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -173,27 +173,26 @@ protected void updateItem(String profit, boolean empty) { }); TableColumn marketColumn = new TableColumn<>("Market"); marketColumn.setMinWidth(100); - marketColumn.setCellFactory(column -> { - return new TableCell() { - private final Button button = new Button("Market Page"); - - @Override - protected void updateItem(Share share, boolean empty) { - super.updateItem(share, empty); - - if (empty) { - setGraphic(null); - } else { - button.setOnAction(event -> { - Share s = getTableView().getItems().get(getIndex()); - getTabPane().getSelectionModel().select(0); - stocksList.getSelectionModel().select(s.getStock()); - showStockChart(s.getStock()); - }); - setGraphic(button); - } + marketColumn.setStyle("-fx-alignment: CENTER;"); + marketColumn.setCellFactory(column -> new TableCell() { + private final Button button = new Button("Market Page"); + + @Override + protected void updateItem(Share share, boolean empty) { + super.updateItem(share, empty); + + if (empty) { + setGraphic(null); + } else { + button.setOnAction(event -> { + Share s = getTableView().getItems().get(getIndex()); + getTabPane().getSelectionModel().select(0); + stocksList.getSelectionModel().select(s.getStock()); + showStockChart(s.getStock()); + }); + setGraphic(button); } - }; + } }); portfolioTable .getColumns() diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index dd7dfba..9f9810d 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -35,7 +35,7 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - Label infoField = new Label("Select stock file, or use default"); + Label infoField = new Label("Select stock file, or press start to use default configuration"); // infoField.setMaxWidth(250); nameField = new TextField("user"); From 08991e16ffd610d3726d2ebf15ea42de0cfcf8fc Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 25 May 2026 20:05:43 +0200 Subject: [PATCH 81/83] feat: sell and quit button --- src/main/java/millions/App.java | 6 +++++- src/main/java/millions/model/Player.java | 8 +++++++ src/main/java/millions/view/ExitView.java | 26 +++++++++++++++++++++++ src/main/java/millions/view/GameView.java | 22 +++++++++++++++++-- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/main/java/millions/view/ExitView.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 9a83f2c..942953a 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -13,6 +13,7 @@ import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.UncheckedFileNotFoundException; +import millions.view.ExitView; import millions.view.GameView; import millions.view.StartView; @@ -35,7 +36,10 @@ public void start(Stage stage) { startView.getSelectedFile().toPath(), startView.getPreRunWeeks()); - GameView gameView = new GameView(controller); + GameView gameView = + new GameView( + controller, + () -> stage.setScene(new Scene(new ExitView(controller.getPlayer()), 400, 250))); controller.getPlayer().addListener(gameView); controller.getExchange().addListener(gameView); diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index e4abeae..515926e 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -63,6 +63,7 @@ public void withdrawMoney(BigDecimal amount) { /** * Calculates the skill level of the player + * * @return String player status level */ public String getStatus() { @@ -101,6 +102,13 @@ public Portfolio getPortfolio() { return this.portfolio; } + /** + * @return player startingMoney + */ + public BigDecimal getStartingMoneh() { + return this.startingMoney; + } + /** * @param share Share to be added */ diff --git a/src/main/java/millions/view/ExitView.java b/src/main/java/millions/view/ExitView.java new file mode 100644 index 0000000..bd87dff --- /dev/null +++ b/src/main/java/millions/view/ExitView.java @@ -0,0 +1,26 @@ +package millions.view; + +import javafx.application.Platform; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import millions.model.Player; + +public class ExitView extends VBox { + + public ExitView(Player player) { + setSpacing(12); + + Label title = new Label("Game over"); + Label weeks = new Label("Weeks traded: " + player.getTransactionArchive().countDistinctWeeks()); + Label trades = new Label("Trades: " + player.getTransactionArchive().getTransactions().size()); + Label netWorth = new Label("Final net worth: " + player.getNetWorth()); + Label netProfit = + new Label("Total profit: " + player.getNetWorth().subtract(player.getStartingMoneh())); + + Button quitButton = new Button("Quit"); + quitButton.setOnAction(event -> Platform.exit()); + + getChildren().addAll(title, weeks, trades, netWorth, quitButton, netProfit); + } +} diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index ad63e4e..494a660 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -57,10 +57,13 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Button buyButton = new Button("Buy"); private final Button sellButton = new Button("Sell"); private final Button advanceButton = new Button("Advance week"); + private final Button sellAllAndQuitButton = new Button("Sell all & quit"); + private final Runnable onSellAllAndQuit; private boolean updatingQuantityControls; - public GameView(GameController controller) { + public GameView(GameController controller, Runnable onSellAllAndQuit) { this.controller = controller; + this.onSellAllAndQuit = onSellAllAndQuit; setTop(createHeader()); setCenter(createTabs()); configureStocksList(); @@ -124,7 +127,8 @@ private Tab createStocksTab() { quantitySlider, buyButton, sellBox, - advanceButton); + advanceButton, + sellAllAndQuitButton); HBox content = new HBox(12, leftPane, rightPane); VBox outer = new VBox(12, content, actionBar, actionStatusLabel); return new Tab("Stocks", outer); @@ -212,6 +216,7 @@ private void configureButtons() { buyButton.setOnAction(event -> buySelectedStock()); sellButton.setOnAction(event -> sellSelectedShare()); advanceButton.setOnAction(event -> advanceWeek()); + sellAllAndQuitButton.setOnAction(event -> sellAllAndQuit()); } private void configureQuantityControls() { @@ -396,6 +401,19 @@ private void advanceWeek() { showStockChart(stocksList.getSelectionModel().getSelectedItem()); } + public void sellAllShares() { + List shares = new java.util.ArrayList<>(controller.getPlayer().getPortfolio().getShares()); + for (Share share : shares) { + controller.sellShare(share); + } + refreshAll(); + } + + private void sellAllAndQuit() { + sellAllShares(); + onSellAllAndQuit.run(); + } + private void setActionStatus(String message, boolean success) { actionStatusLabel.setText(message); actionStatusLabel.setStyle(success ? "-fx-text-fill: green;" : "-fx-text-fill: red;"); From d7c9b06f89932c9b89d9f72326bf79e648adc524 Mon Sep 17 00:00:00 2001 From: martin Date: Tue, 26 May 2026 01:25:47 +0200 Subject: [PATCH 82/83] feat: adding styling to application + Market page --- src/main/java/millions/App.java | 24 +- src/main/java/millions/view/ExitView.java | 69 +++- src/main/java/millions/view/GameView.java | 364 ++++++++++++++++++--- src/main/java/millions/view/StartView.java | 29 +- src/main/java/millions/view/ViewUtils.java | 46 ++- src/main/resources/styles/millions.css | 190 +++++++++++ 6 files changed, 648 insertions(+), 74 deletions(-) create mode 100644 src/main/resources/styles/millions.css diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 942953a..ed5d7c7 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,16 +1,14 @@ package millions; - import java.math.BigDecimal; +import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; - import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.stage.Stage; import millions.controller.GameController; -import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.UncheckedFileNotFoundException; import millions.view.ExitView; @@ -20,6 +18,9 @@ /** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { private static final Logger logger = Logger.getLogger(App.class.getName()); + private static final String STYLESHEET = + Objects.requireNonNull(App.class.getResource("/styles/millions.css")).toExternalForm(); + @Override public void start(Stage stage) { GameController controller = new GameController(); @@ -39,11 +40,18 @@ public void start(Stage stage) { GameView gameView = new GameView( controller, - () -> stage.setScene(new Scene(new ExitView(controller.getPlayer()), 400, 250))); + () -> { + ExitView exitView = new ExitView(controller.getPlayer()); + exitView.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + Scene exitScene = new Scene(exitView, 500, 400); + exitScene.getStylesheets().add(STYLESHEET); + stage.setScene(exitScene); + }); controller.getPlayer().addListener(gameView); controller.getExchange().addListener(gameView); Scene gameScene = new Scene(gameView, 1920, 1080); + gameScene.getStylesheets().add(STYLESHEET); stage.setScene(gameScene); } catch (InvalidFormatException e) { logger.log(Level.WARNING, "InvalidFormatException: " + e.getMessage()); @@ -51,11 +59,12 @@ public void start(Stage stage) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); - alert.setContentText(e.getMessage() + "\nPlease control the format of the selected file"); + alert.setContentText( + e.getMessage() + "\nPlease control the format of the selected file"); alert.showAndWait(); - } catch(UncheckedFileNotFoundException e) { + } catch (UncheckedFileNotFoundException e) { logger.log(Level.WARNING, "FileNotFoundException: " + e.getMessage()); Alert alert = new Alert(Alert.AlertType.ERROR); @@ -72,7 +81,10 @@ public void start(Stage stage) { }); Scene scene = new Scene(startView, 400, 350); + scene.getStylesheets().add(STYLESHEET); stage.setTitle("Millions"); + stage.setMinWidth(900); + stage.setMinHeight(600); stage.setScene(scene); stage.show(); } diff --git a/src/main/java/millions/view/ExitView.java b/src/main/java/millions/view/ExitView.java index bd87dff..f90a9e8 100644 --- a/src/main/java/millions/view/ExitView.java +++ b/src/main/java/millions/view/ExitView.java @@ -1,26 +1,79 @@ package millions.view; +import java.math.BigDecimal; import javafx.application.Platform; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import millions.model.Player; public class ExitView extends VBox { public ExitView(Player player) { - setSpacing(12); + setSpacing(16); + setAlignment(Pos.CENTER); + setPadding(new Insets(60)); - Label title = new Label("Game over"); - Label weeks = new Label("Weeks traded: " + player.getTransactionArchive().countDistinctWeeks()); - Label trades = new Label("Trades: " + player.getTransactionArchive().getTransactions().size()); - Label netWorth = new Label("Final net worth: " + player.getNetWorth()); - Label netProfit = - new Label("Total profit: " + player.getNetWorth().subtract(player.getStartingMoneh())); + Label title = new Label("GAME OVER"); + title.getStyleClass().add("game-over-title"); + HBox titleRow = new HBox(title); + titleRow.setAlignment(Pos.CENTER); + titleRow.setMaxWidth(Double.MAX_VALUE); + + Separator sep = new Separator(); + sep.setMaxWidth(320); + + Label weeks = new Label(String.valueOf(player.getTransactionArchive().countDistinctWeeks())); + Label trades = + new Label(String.valueOf(player.getTransactionArchive().getTransactions().size())); + Label netWorth = new Label(ViewUtils.formatMoney(player.getNetWorth())); + + BigDecimal profit = player.getNetWorth().subtract(player.getStartingMoneh()); + String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + Label netProfit = new Label(sign + ViewUtils.formatMoney(profit)); + netProfit + .getStyleClass() + .add(profit.compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"); + + GridPane stats = new GridPane(); + stats.setHgap(24); + stats.setVgap(12); + ColumnConstraints labelCol = new ColumnConstraints(); + labelCol.setHalignment(HPos.RIGHT); + labelCol.setHgrow(Priority.NEVER); + ColumnConstraints valueCol = new ColumnConstraints(); + valueCol.setHalignment(HPos.LEFT); + valueCol.setHgrow(Priority.ALWAYS); + stats.getColumnConstraints().addAll(labelCol, valueCol); + + stats.addRow(0, statlabel("Weeks traded"), weeks); + stats.addRow(1, statlabel("Trades made"), trades); + stats.addRow(2, statlabel("Final net worth"), netWorth); + stats.addRow(3, statlabel("Total profit"), netProfit); + + for (Label l : new Label[] {weeks, trades, netWorth, netProfit}) { + l.getStyleClass().add("stat-value"); + } Button quitButton = new Button("Quit"); + quitButton.getStyleClass().add("btn-primary"); + quitButton.setPrefWidth(160); quitButton.setOnAction(event -> Platform.exit()); - getChildren().addAll(title, weeks, trades, netWorth, quitButton, netProfit); + getChildren().addAll(titleRow, sep, stats, quitButton); + } + + private static Label statlabel(String text) { + Label l = new Label(text); + l.getStyleClass().add("section-title"); + return l; } } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 494a660..bcdee99 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.util.List; import javafx.beans.property.SimpleStringProperty; // For data binding for table string +import javafx.geometry.Orientation; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -11,6 +12,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.Separator; import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @@ -21,6 +23,7 @@ import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import millions.controller.GameController; import millions.model.Exchange; @@ -30,6 +33,8 @@ import millions.model.Share; import millions.model.Stock; import millions.model.Transaction; +import millions.model.calculators.PurchaseCalculator; +import millions.model.calculators.SaleCalculator; /** Main game screen with tabs */ public class GameView extends BorderPane implements PlayerListener, ExchangeListener { @@ -41,6 +46,13 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Label netWorthLabel = new Label(); private final Label statusLabel = new Label(); + { + for (Label l : + new Label[] {playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel}) { + l.getStyleClass().add("stat-value"); + } + } + private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); private final TableView portfolioTable = new TableView<>(); @@ -58,6 +70,22 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Button sellButton = new Button("Sell"); private final Button advanceButton = new Button("Advance week"); private final Button sellAllAndQuitButton = new Button("Sell all & quit"); + + private final Label stockHighLabel = new Label(); + private final Label stockLowLabel = new Label(); + private final Label stockChangeLabel = new Label(); + private final Label buyCostPreviewLabel = new Label(); + private final Label sellCostPreviewLabel = new Label(); + private final TableView gainersTable = new TableView<>(); + private final TableView losersTable = new TableView<>(); + private final TextField transactionSearchField = new TextField(); + + { + buyButton.getStyleClass().add("btn-primary"); + advanceButton.getStyleClass().add("btn-primary"); + sellAllAndQuitButton.getStyleClass().add("btn-danger"); + } + private final Runnable onSellAllAndQuit; private boolean updatingQuantityControls; @@ -73,25 +101,51 @@ public GameView(GameController controller, Runnable onSellAllAndQuit) { } private HBox createHeader() { - Label title = new Label("Millions"); - title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + HBox title = ViewUtils.buildTitle(); + + javafx.scene.layout.Region spacer = new javafx.scene.layout.Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); HBox header = - new HBox(20, title, playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel); + new HBox( + 20, + title, + statBox("PLAYER", playerNameLabel), + statBox("WEEK", weekLabel), + statBox("CASH", moneyLabel), + statBox("NET WORTH", netWorthLabel), + statBox("STATUS", statusLabel), + spacer, + sellAllAndQuitButton); + header.getStyleClass().add("header-panel"); + header.setAlignment(javafx.geometry.Pos.CENTER_LEFT); return header; } + private static VBox statBox(String key, Label valueLabel) { + Label keyLabel = new Label(key); + keyLabel.getStyleClass().add("section-title"); + VBox box = new VBox(1, keyLabel, valueLabel); + box.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + return box; + } + private TabPane createTabs() { TabPane tabPane = new TabPane(); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.getTabs().add(createStocksTab()); tabPane.getTabs().add(createPortfolioTab()); tabPane.getTabs().add(createTransactionsTab()); + tabPane.getTabs().add(createMarketTab()); return tabPane; } private Tab createStocksTab() { - VBox leftPane = new VBox(10, new Label("Search"), searchField, stocksList); + VBox leftPane = new VBox(10, searchField, stocksList); + leftPane.setPrefWidth(500); + stocksList.setPrefWidth(500); + stocksList.setMinWidth(500); + stocksList.setMaxWidth(Double.MAX_VALUE); searchField.setPromptText("Search"); searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshStocks()); @@ -106,10 +160,20 @@ private Tab createStocksTab() { stockChart.setCreateSymbols(true); stockChart.setAnimated(false); stockChart.setPrefHeight(500); + stockChart.setMaxWidth(Double.MAX_VALUE); - VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + selectedStockLabel.getStyleClass().add("selected-stock-label"); - quantityField.setPrefWidth(70); + stockHighLabel.getStyleClass().add("section-title"); + stockLowLabel.getStyleClass().add("section-title"); + stockChangeLabel.getStyleClass().add("section-title"); + HBox stockStats = new HBox(20, stockHighLabel, stockLowLabel, stockChangeLabel); + + VBox rightPane = new VBox(6, selectedStockLabel, stockStats, stockChart); + rightPane.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(rightPane, Priority.ALWAYS); + + quantityField.setPrefWidth(100); quantityField.setTextFormatter( new TextFormatter<>( change -> change.getControlNewText().matches("-?\\d*") ? change : null)); @@ -117,19 +181,36 @@ private Tab createStocksTab() { ViewUtils.configureOwnedSharesBox(ownedSharesBox); - HBox sellBox = new HBox(8, sellButton, new VBox(4, new Label("Owned Shares"), ownedSharesBox)); + Label buyHeader = new Label("BUY"); + buyHeader.getStyleClass().add("section-title"); + HBox buyControls = new HBox(8, quantityField, quantitySlider, buyButton); + buyControls.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + buyCostPreviewLabel.getStyleClass().add("cost-preview"); + VBox buySection = new VBox(4, buyHeader, buyControls, buyCostPreviewLabel); + + javafx.scene.control.Separator sep1 = + new javafx.scene.control.Separator(javafx.geometry.Orientation.VERTICAL); + + Label sellHeader = new Label("SELL"); + sellHeader.getStyleClass().add("section-title"); + HBox sellControls = new HBox(8, ownedQuantityLabel, ownedSharesBox, sellButton); + sellControls.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + sellCostPreviewLabel.getStyleClass().add("cost-preview"); + VBox sellSection = new VBox(4, sellHeader, sellControls, sellCostPreviewLabel); + + javafx.scene.control.Separator sep2 = + new javafx.scene.control.Separator(javafx.geometry.Orientation.VERTICAL); + + Label weekHeader = new Label("WEEK"); + weekHeader.getStyleClass().add("section-title"); + VBox weekSection = new VBox(4, weekHeader, advanceButton); + + HBox actionBar = new HBox(16, buySection, sep1, sellSection, sep2, weekSection); + actionBar.getStyleClass().add("toolbar-panel"); + actionBar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - HBox actionBar = - new HBox( - 10, - ownedQuantityLabel, - quantityField, - quantitySlider, - buyButton, - sellBox, - advanceButton, - sellAllAndQuitButton); HBox content = new HBox(12, leftPane, rightPane); + HBox.setHgrow(content, Priority.ALWAYS); VBox outer = new VBox(12, content, actionBar, actionStatusLabel); return new Tab("Stocks", outer); } @@ -138,39 +219,47 @@ private Tab createPortfolioTab() { portfolioTable.setPlaceholder(new Label("No shares yet")); TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setPrefWidth(200); symbolColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getStock().getSymbol())); TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setPrefWidth(100); quantityColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getQuantity().toPlainString())); TableColumn purchasePriceColumn = new TableColumn<>("Purchase price"); + purchasePriceColumn.setPrefWidth(150); purchasePriceColumn.setCellValueFactory( - data -> new SimpleStringProperty(data.getValue().getPurchasePrice().toPlainString())); + data -> + new SimpleStringProperty(ViewUtils.formatMoney(data.getValue().getPurchasePrice()))); TableColumn currentPriceColumn = new TableColumn<>("Current price"); + currentPriceColumn.setPrefWidth(150); currentPriceColumn.setCellValueFactory( data -> - new SimpleStringProperty(data.getValue().getStock().getSalesPrice().toPlainString())); + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getStock().getSalesPrice()))); TableColumn profitColumn = new TableColumn<>("Profit"); + profitColumn.setPrefWidth(100); profitColumn.setCellValueFactory( data -> - new SimpleStringProperty(ViewUtils.getShareProfit(data.getValue()).toPlainString())); + new SimpleStringProperty( + ViewUtils.formatMoney(ViewUtils.getShareProfit(data.getValue())))); profitColumn.setCellFactory( column -> new TableCell<>() { @Override protected void updateItem(String profit, boolean empty) { super.updateItem(profit, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty) { setText(null); - setStyle(""); return; } setText(profit); - setStyle(profit.startsWith("-") ? "-fx-text-fill: red;" : "-fx-text-fill: green;"); + getStyleClass().add(profit.startsWith("-") ? "status-error" : "status-success"); } }); @@ -185,31 +274,73 @@ private Tab createTransactionsTab() { transactionsTable.setPlaceholder(new Label("No transactions yet")); TableColumn typeColumn = new TableColumn<>("Type"); + typeColumn.setPrefWidth(100); typeColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getClass().getSimpleName())); TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setPrefWidth(100); symbolColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getShare().getStock().getSymbol())); TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setPrefWidth(100); quantityColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getShare().getQuantity().toPlainString())); TableColumn weekColumn = new TableColumn<>("Week"); + weekColumn.setPrefWidth(100); weekColumn.setCellValueFactory( data -> new SimpleStringProperty(String.valueOf(data.getValue().getWeek()))); + TableColumn grossColumn = new TableColumn<>("Gross"); + grossColumn.setPrefWidth(150); + grossColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateGross()))); + + TableColumn commissionColumn = new TableColumn<>("Commission"); + commissionColumn.setPrefWidth(150); + commissionColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateCommission()))); + + TableColumn taxColumn = new TableColumn<>("Tax"); + taxColumn.setPrefWidth(150); + taxColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateTax()))); + TableColumn totalColumn = new TableColumn<>("Total"); + totalColumn.setPrefWidth(150); totalColumn.setCellValueFactory( data -> new SimpleStringProperty( - data.getValue().getCalculator().calculateTotal().toPlainString())); + ViewUtils.formatMoney(data.getValue().getCalculator().calculateTotal()))); transactionsTable .getColumns() - .addAll(typeColumn, symbolColumn, quantityColumn, weekColumn, totalColumn); - return new Tab("Transactions", transactionsTable); + .addAll( + typeColumn, + symbolColumn, + quantityColumn, + weekColumn, + grossColumn, + commissionColumn, + taxColumn, + totalColumn); + + transactionSearchField.setPromptText("Search by symbol or type..."); + transactionSearchField + .textProperty() + .addListener((obs, oldVal, newVal) -> refreshTransactions()); + + VBox content = new VBox(8, transactionSearchField, transactionsTable); + VBox.setVgrow(transactionsTable, Priority.ALWAYS); + return new Tab("Transactions", content); } private void configureButtons() { @@ -217,6 +348,10 @@ private void configureButtons() { sellButton.setOnAction(event -> sellSelectedShare()); advanceButton.setOnAction(event -> advanceWeek()); sellAllAndQuitButton.setOnAction(event -> sellAllAndQuit()); + ownedSharesBox + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldShare, newShare) -> updateSellCostPreview(newShare)); } private void configureQuantityControls() { @@ -254,6 +389,7 @@ private void refreshAll() { refreshStocks(); refreshPortfolio(); refreshTransactions(); + refreshMarket(); } private void refreshPlayerInfo() { @@ -264,11 +400,11 @@ private void refreshPlayerInfo() { return; } - playerNameLabel.setText("Player: " + player.getName()); - weekLabel.setText("Week: " + exchange.getWeekNumber()); - moneyLabel.setText("Money: " + player.getMoney()); - netWorthLabel.setText("Net worth: " + player.getNetWorth()); - statusLabel.setText("Status: " + player.getStatus()); + playerNameLabel.setText(player.getName()); + weekLabel.setText(String.valueOf(exchange.getWeekNumber())); + moneyLabel.setText(ViewUtils.formatMoney(player.getMoney())); + netWorthLabel.setText(ViewUtils.formatMoney(player.getNetWorth())); + statusLabel.setText(player.getStatus()); } private void refreshStocks() { @@ -292,11 +428,22 @@ private void showStockChart(Stock stock) { if (stock == null) { selectedStockLabel.setText("Select a stock to see chart"); + stockHighLabel.setText(""); + stockLowLabel.setText(""); + stockChangeLabel.setText(""); refreshQuantityControls(null); return; } selectedStockLabel.setText(stock.getSymbol() + " - " + stock.getCompany()); + + BigDecimal change = stock.getLatestPriceChange(); + String changeSign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + stockHighLabel.setText("High: " + ViewUtils.formatMoney(stock.getHighestPrice())); + stockLowLabel.setText("Low: " + ViewUtils.formatMoney(stock.getLowestPrice())); + stockChangeLabel.setText("Change: " + changeSign + ViewUtils.formatMoney(change)); + stockChangeLabel.getStyleClass().removeAll("status-success", "status-error"); + stockChangeLabel.getStyleClass().add(change.compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"); refreshQuantityControls(stock); XYChart.Series series = new XYChart.Series<>(); @@ -338,6 +485,8 @@ private void refreshQuantityControls(Stock stock) { if (!ownedSharesBox.getItems().isEmpty()) { ownedSharesBox.getSelectionModel().selectFirst(); } + updateBuyCostPreview(stock, current); + updateSellCostPreview(ownedSharesBox.getSelectionModel().getSelectedItem()); } private void refreshPortfolio() { @@ -348,11 +497,23 @@ private void refreshPortfolio() { private void refreshTransactions() { Player player = controller.getPlayer(); - if (player == null) { - return; - } + if (player == null) return; - transactionsTable.getItems().setAll(player.getTransactionArchive().getTransactions()); + String filter = transactionSearchField.getText().trim().toLowerCase(); + List all = player.getTransactionArchive().getTransactions(); + if (filter.isBlank()) { + transactionsTable.getItems().setAll(all); + } else { + transactionsTable + .getItems() + .setAll( + all.stream() + .filter( + t -> + t.getShare().getStock().getSymbol().toLowerCase().contains(filter) + || t.getClass().getSimpleName().toLowerCase().contains(filter)) + .toList()); + } } private void buySelectedStock() { @@ -365,7 +526,17 @@ private void buySelectedStock() { try { Transaction transaction = controller.buyStock(selectedStock.getSymbol(), BigDecimal.valueOf(getQuantityValue())); - setActionStatus("Bought " + selectedStock.getSymbol(), true); + var calc = transaction.getCalculator(); + setActionStatus( + "Bought " + + selectedStock.getSymbol() + + " — Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Total: " + + ViewUtils.formatMoney(calc.calculateTotal()), + true); refreshAll(); showStockChart(selectedStock); } catch (RuntimeException ex) { @@ -387,7 +558,19 @@ private void sellSelectedShare() { try { Transaction transaction = controller.sellShare(selectedShare); - setActionStatus("Sold " + selectedStock.getSymbol(), true); + var calc = transaction.getCalculator(); + setActionStatus( + "Sold " + + selectedStock.getSymbol() + + " — Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Tax: " + + ViewUtils.formatMoney(calc.calculateTax()) + + " Net: " + + ViewUtils.formatMoney(calc.calculateTotal()), + true); refreshAll(); showStockChart(selectedStock); } catch (RuntimeException ex) { @@ -402,7 +585,8 @@ private void advanceWeek() { } public void sellAllShares() { - List shares = new java.util.ArrayList<>(controller.getPlayer().getPortfolio().getShares()); + List shares = + new java.util.ArrayList<>(controller.getPlayer().getPortfolio().getShares()); for (Share share : shares) { controller.sellShare(share); } @@ -416,7 +600,8 @@ private void sellAllAndQuit() { private void setActionStatus(String message, boolean success) { actionStatusLabel.setText(message); - actionStatusLabel.setStyle(success ? "-fx-text-fill: green;" : "-fx-text-fill: red;"); + actionStatusLabel.getStyleClass().removeAll("status-success", "status-error"); + actionStatusLabel.getStyleClass().add(success ? "status-success" : "status-error"); } private int getQuantityValue() { @@ -444,6 +629,7 @@ private void syncQuantityFromField(String newValue) { quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; + updateBuyCostPreview(stocksList.getSelectionModel().getSelectedItem(), clamped); } private void syncQuantityFromSlider(int newValue) { @@ -456,6 +642,7 @@ private void syncQuantityFromSlider(int newValue) { quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; + updateBuyCostPreview(stocksList.getSelectionModel().getSelectedItem(), clamped); } private int parseQuantity(String text) { @@ -479,6 +666,107 @@ private String formatStock(Stock stock) { return ViewUtils.formatStock(stock); } + private void updateBuyCostPreview(Stock stock, int quantity) { + if (stock == null || quantity <= 0) { + buyCostPreviewLabel.setText(""); + return; + } + Share tempShare = new Share(stock, BigDecimal.valueOf(quantity), stock.getSalesPrice()); + PurchaseCalculator calc = new PurchaseCalculator(tempShare); + buyCostPreviewLabel.setText( + "Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Total: " + + ViewUtils.formatMoney(calc.calculateTotal())); + } + + private void updateSellCostPreview(Share share) { + if (share == null) { + sellCostPreviewLabel.setText(""); + return; + } + SaleCalculator calc = new SaleCalculator(share); + sellCostPreviewLabel.setText( + "Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Tax: " + + ViewUtils.formatMoney(calc.calculateTax()) + + " Net: " + + ViewUtils.formatMoney(calc.calculateTotal())); + } + + private Tab createMarketTab() { + gainersTable.setPlaceholder(new Label("Advance a week to see data")); + losersTable.setPlaceholder(new Label("Advance a week to see data")); + + for (TableView table : new TableView[] {gainersTable, losersTable}) { + TableColumn symCol = new TableColumn<>("Symbol"); + symCol.setPrefWidth(100); + symCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().getSymbol())); + + TableColumn nameCol = new TableColumn<>("Company"); + nameCol.setPrefWidth(200); + nameCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().getCompany())); + + TableColumn priceCol = new TableColumn<>("Price"); + priceCol.setPrefWidth(100); + priceCol.setCellValueFactory( + d -> new SimpleStringProperty(ViewUtils.formatMoney(d.getValue().getSalesPrice()))); + + TableColumn changeCol = new TableColumn<>("Change"); + changeCol.setPrefWidth(100); + changeCol.setCellValueFactory( + d -> { + BigDecimal change = d.getValue().getLatestPriceChange(); + String sign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + return new SimpleStringProperty(sign + ViewUtils.formatMoney(change)); + }); + changeCol.setCellFactory( + col -> + new TableCell<>() { + @Override + protected void updateItem(String val, boolean empty) { + super.updateItem(val, empty); + getStyleClass().removeAll("status-success", "status-error"); + if (empty || val == null) { + setText(null); + return; + } + setText(val); + getStyleClass().add(val.startsWith("+") ? "status-success" : "status-error"); + } + }); + table.getColumns().addAll(symCol, nameCol, priceCol, changeCol); + } + + Label gainersTitle = new Label("TOP GAINERS"); + gainersTitle.getStyleClass().add("section-title"); + Label losersTitle = new Label("TOP LOSERS"); + losersTitle.getStyleClass().add("section-title"); + + VBox gainersBox = new VBox(6, gainersTitle, gainersTable); + VBox.setVgrow(gainersTable, Priority.ALWAYS); + VBox losersBox = new VBox(6, losersTitle, losersTable); + VBox.setVgrow(losersTable, Priority.ALWAYS); + + HBox content = new HBox(16, gainersBox, new Separator(Orientation.VERTICAL), losersBox); + HBox.setHgrow(gainersBox, Priority.ALWAYS); + HBox.setHgrow(losersBox, Priority.ALWAYS); + content.setPadding(new javafx.geometry.Insets(12)); + return new Tab("Market", content); + } + + private void refreshMarket() { + Exchange exchange = controller.getExchange(); + if (exchange == null) return; + gainersTable.getItems().setAll(exchange.getGainers(10)); + losersTable.getItems().setAll(exchange.getLosers(10)); + } + // Listener callbacks update the shared header and the stocks tab. @Override public void onMoneyChanged(BigDecimal newBalance) { diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index dd7dfba..481cff3 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -8,7 +8,6 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; - import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -18,7 +17,6 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; -import millions.App; /** The initial game setup screen where the player enters their info. */ public class StartView extends VBox { @@ -35,9 +33,6 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - Label infoField = new Label("Select stock file, or use default"); - // infoField.setMaxWidth(250); - nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); @@ -91,19 +86,20 @@ public StartView(Stage stage) { }); startButton = new Button("Start game"); + startButton.getStyleClass().add("btn-primary"); startButton.setDisable(true); - Label title = new Label("Millions"); - title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + javafx.scene.layout.HBox title = ViewUtils.buildTitle(); + title.getStyleClass().add("title-hero"); + title.setAlignment(javafx.geometry.Pos.CENTER); getChildren() .addAll( title, - nameField, - startingAmountField, - preRunWeeksField, - infoField, - filepickerButton, + fieldBox("Player name", nameField), + fieldBox("Starting amount ($)", startingAmountField), + fieldBox("Pre-run weeks", preRunWeeksField), + fieldBox("Stock file", filepickerButton), startButton); checkStartButtonValid(); @@ -167,4 +163,13 @@ public File getSelectedFile() { public Button getStartButton() { return startButton; } + + private static javafx.scene.layout.VBox fieldBox(String labelText, javafx.scene.Node field) { + Label label = new Label(labelText); + label.getStyleClass().add("section-title"); + javafx.scene.layout.VBox box = new javafx.scene.layout.VBox(4, label, field); + box.setAlignment(Pos.CENTER_LEFT); + box.setMaxWidth(250); + return box; + } } diff --git a/src/main/java/millions/view/ViewUtils.java b/src/main/java/millions/view/ViewUtils.java index 5500438..a554e0d 100644 --- a/src/main/java/millions/view/ViewUtils.java +++ b/src/main/java/millions/view/ViewUtils.java @@ -1,8 +1,12 @@ package millions.view; import java.math.BigDecimal; +import java.math.RoundingMode; +import javafx.geometry.Pos; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; +import javafx.scene.layout.HBox; import millions.model.Share; import millions.model.Stock; import millions.model.Transaction; @@ -12,6 +16,20 @@ public final class ViewUtils { private ViewUtils() {} + public static String formatMoney(BigDecimal value) { + return value.setScale(2, RoundingMode.HALF_UP).toPlainString() + "$"; + } + + public static HBox buildTitle() { + Label million = new Label("MILLION"); + million.getStyleClass().add("title-main"); + Label dollar = new Label("$"); + dollar.getStyleClass().add("title-dollar"); + HBox box = new HBox(0, million, dollar); + box.setAlignment(Pos.CENTER_LEFT); + return box; + } + public static void configureOwnedSharesBox(ComboBox comboBox) { comboBox.setCellFactory( box -> @@ -19,12 +37,12 @@ public static void configureOwnedSharesBox(ComboBox comboBox) { @Override protected void updateItem(Share share, boolean empty) { super.updateItem(share, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty || share == null) { setText(null); - setStyle(""); } else { setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); + getStyleClass().add(getProfitClass(share)); } } }); @@ -33,31 +51,39 @@ protected void updateItem(Share share, boolean empty) { @Override protected void updateItem(Share share, boolean empty) { super.updateItem(share, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty || share == null) { setText("Select lot"); - setStyle(""); } else { setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); + getStyleClass().add(getProfitClass(share)); } } }); } public static String formatStock(Stock stock) { - return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + return stock.getSymbol() + + " - " + + stock.getCompany() + + " (" + + formatMoney(stock.getSalesPrice()) + + ")"; } public static String formatOwnedShare(Share share) { BigDecimal profit = getShareProfit(share); String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; - return share.getQuantity() + "|" + share.getPurchasePrice() + "|" + sign + profit; + return share.getQuantity() + + " @ " + + formatMoney(share.getPurchasePrice()) + + " " + + sign + + formatMoney(profit); } - public static String getProfitStyle(Share share) { - return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 - ? "-fx-text-fill: green;" - : "-fx-text-fill: red;"; + public static String getProfitClass(Share share) { + return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"; } public static BigDecimal getShareProfit(Share share) { diff --git a/src/main/resources/styles/millions.css b/src/main/resources/styles/millions.css new file mode 100644 index 0000000..8e05692 --- /dev/null +++ b/src/main/resources/styles/millions.css @@ -0,0 +1,190 @@ +.root { + -fx-font-size: 14px; + -fx-background-color: bg; + + bg: #1e1e2e; + bg-elevated: #252535; + bg-hover: #2a2a3e; + bg-component: #2e2e42; + bg-selected: #1a3a2a; + border: #3a3a4a; + text-primary: #e8e8f0; + text-muted: #8888a0; + accent: #22c55e; + danger: #ef4444; +} + +.header-panel { + -fx-background-color: bg; + -fx-padding: 10px 16px; + -fx-border-color: accent border border border; + -fx-border-width: 2px 0 1px 0; + -fx-spacing: 20px; +} + +.toolbar-panel { + -fx-background-color: bg; + -fx-padding: 10px 14px; + -fx-border-color: border; + -fx-border-width: 1px 0 0 0; +} + +.label { -fx-text-fill: text-primary; } +.status-success { -fx-text-fill: accent; } +.status-error { -fx-text-fill: danger; } + +.title { + -fx-font-size: 32px; + -fx-font-weight: bold; + -fx-text-fill: accent; +} + +.title-main, .title-dollar, .game-over-title { + -fx-font-weight: bold; + -fx-font-family: "Georgia"; + -fx-font-style: italic; +} + +.title-main { -fx-font-size: 32px; -fx-text-fill: text-primary; } +.title-dollar { -fx-font-size: 36px; -fx-text-fill: accent; } +.game-over-title { -fx-font-size: 56px; -fx-text-fill: text-primary; } + +.title-hero .title-main { -fx-font-size: 80px; } +.title-hero .title-dollar { -fx-font-size: 88px; } + +.selected-stock-label { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: accent; +} + +.section-title { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: text-muted; +} + +.cost-preview { -fx-font-size: 11px; -fx-text-fill: text-muted; -fx-font-family: "monospace"; } +.stat-value { -fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: text-primary; } + +.button { + -fx-background-color: bg-component; + -fx-text-fill: text-primary; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; + -fx-padding: 6px 14px; + -fx-cursor: hand; +} + +.button:hover { -fx-background-color: #3a3a52; -fx-border-color: accent; } +.button:pressed { -fx-background-color: #22283a; } +.button:disabled { -fx-opacity: 0.4; } + +.btn-primary { + -fx-background-color: derive(accent, -20%); + -fx-text-fill: white; + -fx-border-color: accent; + -fx-font-weight: bold; +} + +.btn-primary:hover { -fx-background-color: accent; -fx-border-color: derive(accent, 25%); } +.btn-primary:pressed { -fx-background-color: derive(accent, -35%); } + +.btn-danger { + -fx-background-color: derive(danger, -65%); + -fx-text-fill: derive(danger, 40%); + -fx-border-color: danger; +} + +.btn-danger:hover { -fx-background-color: derive(danger, -55%); -fx-border-color: derive(danger, 15%); } +.btn-danger:pressed { -fx-background-color: derive(danger, -75%); } + +.text-field { + -fx-background-color: bg; + -fx-text-fill: text-primary; + -fx-prompt-text-fill: text-muted; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; + -fx-padding: 5px 10px; +} + +.text-field:focused, +.combo-box-base:focused { -fx-border-color: accent; } + +.combo-box, .combo-box-base { + -fx-background-color: bg-component; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; +} + +.combo-box-base .list-cell { -fx-text-fill: text-primary; } +.combo-box-popup .list-view { -fx-background-color: bg-elevated; -fx-border-color: border; } + +.tab-header-background { -fx-background-color: bg; } + +.tab-pane .tab { + -fx-background-color: bg-elevated; + -fx-background-radius: 6 6 0 0; + -fx-padding: 6px 16px; +} + +.tab-pane .tab:selected { + -fx-background-color: bg; + -fx-border-color: accent transparent transparent transparent; + -fx-border-width: 2px 0 0 0; +} + +.tab-pane .tab-label { -fx-text-fill: text-muted; -fx-font-size: 13px; -fx-font-weight: bold; } +.tab-pane .tab:selected .tab-label { -fx-text-fill: text-primary; } + +.slider .track { -fx-background-color: border; -fx-background-radius: 4px; } +.slider .thumb { -fx-background-color: accent; -fx-background-radius: 50%; -fx-effect: none; } + +.list-view, .table-view { + -fx-background-color: bg; + -fx-control-inner-background: bg; + -fx-border-color: border; + -fx-border-width: 1px; +} + +.list-view { -fx-background-radius: 6px; } +.table-view { -fx-table-cell-border-color: bg-component; -fx-table-header-border-color: border; } + +.table-view .column-header, +.table-view .filler { + -fx-background-color: bg-elevated; + -fx-border-color: border; + -fx-border-width: 0 1px 1px 0; + -fx-padding: 6px 8px; +} + +.table-row-cell { + -fx-background-color: bg; + -fx-text-background-color: text-primary; + -fx-border-color: transparent transparent bg-component transparent; + -fx-border-width: 1px; +} + +/* Every other cell differently collored */ +.table-row-cell:odd, .list-cell:odd { + -fx-background-color: bg-elevated; +} + +.table-row-cell:hover, .list-cell:hover { + -fx-background-color: bg-hover; +} +.table-row-cell:selected { -fx-background-color: bg-selected; } +.table-cell { -fx-text-fill: text-primary; -fx-padding: 6px 8px; } + +.list-cell { -fx-background-color: transparent; -fx-text-fill: text-primary; -fx-padding: 6px 10px; } +.list-cell:selected { -fx-background-color: bg-selected; -fx-text-fill: derive(accent, 25%); } + +.chart, .chart-plot-background, .chart-content { -fx-background-color: bg; } +.chart { -fx-padding: 10px; } +.chart-series-line { -fx-stroke: accent; -fx-stroke-width: 2px; } From d433511ec473c3dcf1bfd6c688b08cc4a2f12a9d Mon Sep 17 00:00:00 2001 From: martin Date: Tue, 26 May 2026 12:16:16 +0200 Subject: [PATCH 83/83] added default-stocks file with up to date format --- src/main/resources/data/default-stocks.csv | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/resources/data/default-stocks.csv b/src/main/resources/data/default-stocks.csv index 4a364c2..4340bf3 100644 --- a/src/main/resources/data/default-stocks.csv +++ b/src/main/resources/data/default-stocks.csv @@ -1,20 +1,20 @@ # Default stock data for Millions -# symbol,name,price -AAPL,Pear Inc.,276.43 -MSFT,MacroHard,404.68 -GOOGL,Googly Eyes Inc.,187.34 -AMZN,The Great Bazillion,214.10 -TSLA,Formerly Twitter,342.58 -NVDA,GPUs,191.27 -META,Still Facebook,593.11 -NFLX,And Chill,982.44 -AMD,Advanced Meme Devices,156.79 -JPM,Just Plain Money Chase,245.33 -WFC,Wealthy Folks Credit,82.15 -BAC,Bank of Awkward Capital,48.92 -DIS,Disknee,112.55 -KO,Cocaine,67.14 -PEP,Pepe Cola Co.,159.03 -IBM,Incredibly Boring Machines,241.07 -ORCL,Databases?,171.62 -SAP,Sadly Applying Patches,292.88 +# symbol,name,price,drift;volatility;cycleSpeed;cycleNoise;explosionProbability;explosionSize +AAPL,Pear Inc.,276.43,0.0015;0.022;0.35;0.015;0.015;0.10 +MSFT,MacroHard,404.68,0.0015;0.018;0.30;0.013;0.012;0.08 +GOOGL,Googly Eyes Inc.,187.34,0.002;0.027;0.40;0.018;0.020;0.12 +AMZN,The Great Bazillion,214.10,0.0015;0.024;0.32;0.015;0.015;0.10 +TSLA,Formerly Twitter,342.58,0.002;0.045;0.50;0.027;0.030;0.20 +NVDA,GPUs,191.27,0.002;0.033;0.42;0.021;0.020;0.14 +META,Still Facebook,593.11,0.0015;0.021;0.28;0.014;0.012;0.09 +NFLX,And Chill,982.44,0.002;0.038;0.45;0.024;0.025;0.18 +AMD,Advanced Meme Devices,156.79,0.002;0.036;0.48;0.022;0.022;0.16 +JPM,Just Plain Money Chase,245.33,0.001;0.015;0.20;0.009;0.010;0.06 +WFC,Wealthy Folks Credit,82.15,0.001;0.015;0.18;0.009;0.010;0.05 +BAC,Bank of Awkward Capital,48.92,0.001;0.016;0.18;0.009;0.010;0.05 +DIS,Disknee,112.55,0.0015;0.021;0.30;0.013;0.012;0.08 +KO,Cocaine,67.14,0.001;0.014;0.22;0.008;0.009;0.05 +PEP,Pepe Cola Co.,159.03,0.001;0.015;0.24;0.009;0.010;0.06 +IBM,Incredibly Boring Machines,241.07,0.001;0.014;0.20;0.008;0.009;0.05 +ORCL,Databases?,171.62,0.0015;0.018;0.26;0.011;0.012;0.07 +SAP,Sadly Applying Patches,292.88,0.0015;0.021;0.28;0.013;0.012;0.08