diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java
index b73f634..a3a9af3 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java
@@ -1,31 +1,25 @@
package edu.ntnu.idi.idatt2003.g40.mappe.engine;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Player;
-import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase;
-import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Share;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction;
import edu.ntnu.idi.idatt2003.g40.mappe.service.*;
-import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData;
-import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager;
-import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventPublisher;
-import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType;
import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator;
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
/**
* Represents a stock exchange where stocks can be traded.
*
- *
Holds a map of stocks where stock symbol is key and stock is value.
+ * Holds a map of {@link Stock} objects where stock symbol is key and
+ * stock is value.
*
* Delegates buying and selling to player elements using calculators
*
@@ -33,6 +27,8 @@
*
* @see Player
* @see TransactionCalculator
+ *
+ * @version 1.0.0
* */
public final class Exchange {
@@ -59,13 +55,16 @@ public final class Exchange {
/**
* Constructor.
*
- * @param name name of exchange.
- * @param stocks list of {@link Stock} objects.
+ * @param name name of exchange.
+ * @param stocks list of {@link Stock} objects.
*
* @throws IllegalArgumentException if name or stocks are empty/null.
* */
- public Exchange(final String name, final List stocks) throws IllegalArgumentException {
- if (!Validator.NOT_EMPTY.isValid(name) || stocks == null || stocks.isEmpty()) {
+ public Exchange(final String name, final List stocks)
+ throws IllegalArgumentException {
+ if (!Validator.NOT_EMPTY.isValid(name)
+ || stocks == null
+ || stocks.isEmpty()) {
throw new IllegalArgumentException("Invalid exchange parameters!");
}
this.name = name;
@@ -108,7 +107,7 @@ public IntegerProperty weekProperty() {
*
* @param symbol the stock symbol.
*
- * @return true or false.
+ * @return true or false.
* */
public boolean hasStock(final String symbol) {
return stockMap.containsKey(symbol);
@@ -117,16 +116,16 @@ public boolean hasStock(final String symbol) {
/**
* Getter method for stock element.
*
- * @param symbol the symbol of the stock to get.
+ * @param symbol the symbol of the stock to get.
*
- * @return {@link Stock} element gotten.
+ * @return {@link Stock} element gotten.
*
* @throws IllegalArgumentException if symbol is invalid.
* */
public Stock getStock(final String symbol) throws IllegalArgumentException {
- if (!Validator.VALID_STOCK_NAME.isValid(symbol)) {
+ if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) {
throw new IllegalArgumentException(
- Validator.VALID_STOCK_NAME.getErrorMessage());
+ Validator.VALID_STOCK_SYMBOL.getErrorMessage());
}
return stockMap.get(symbol);
}
@@ -136,7 +135,7 @@ public Stock getStock(final String symbol) throws IllegalArgumentException {
*
* @param searchTerm the term to search for.
*
- * @return a list of {@link Stock} objects.
+ * @return a list of {@link Stock} objects.
* */
public List findStocks(final String searchTerm) {
List result = new ArrayList<>();
@@ -154,24 +153,26 @@ public List findStocks(final String searchTerm) {
/**
* Method called when a player buys a stock.
*
- * @param symbol the stock this player buys.
- * @param quantity the amount of stock to buy.
- * @param player the player buying stock.
+ * @param symbol the stock this player buys.
+ * @param quantity the amount of stock to buy.
+ * @param player the player buying stock.
*
- * @return Transaction representing the transaction.
+ * @return Transaction representing the transaction.
*
* @throws IllegalArgumentException if symbol or player is invalid.
* */
public Transaction buy(final String symbol,
final BigDecimal quantity,
final Player player) throws IllegalArgumentException {
- if (!Validator.VALID_STOCK_NAME.isValid(symbol) || player == null) {
+ if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) || player == null) {
throw new IllegalArgumentException("Invalid purchase!");
}
Stock stock = stockMap.get(symbol);
Share share = new Share(stock, quantity, stock.getSalesPrice());
TransactionCalculator calculator = new PurchaseCalculator(share);
- Purchase purchase = new Purchase(share, week.get(), calculator);
+ Transaction purchase = TransactionFactory.createTransaction(
+ TransactionType.PURCHASE, share, getWeek(), calculator
+ );
player.handleTransaction(purchase);
return purchase;
}
@@ -179,10 +180,10 @@ public Transaction buy(final String symbol,
/**
* Method called when a player sells share.
*
- * @param share the share to sell.
- * @param player the player buying stock.
+ * @param share the share to sell.
+ * @param player the player buying stock.
*
- * @return Transaction representing the transaction.
+ * @return Transaction representing the transaction.
*
* @throws IllegalArgumentException if share or player is null.
* */
@@ -192,27 +193,37 @@ public Transaction sell(final Share share, final Player player)
throw new IllegalArgumentException("Invalid sell!");
}
TransactionCalculator calculator = new SaleCalculator(share);
- Sale sale = new Sale(share, week.get(), calculator);
+ Transaction sale = TransactionFactory.createTransaction(
+ TransactionType.SALE, share, getWeek(), calculator
+ );
player.handleTransaction(sale);
return sale;
}
/**
- * Method called when a player sells share.
+ * Method called when a player sells share,
+ * defined by an amount instead of specific {@link Share} object.
+ *
+ * Might split shares into multiples to ensure proper selling.
+ * Uses the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio}
+ * to split.
*
- * @param amount the amount of "shares" to sell.
+ * @param amount the amount of "shares" to sell.
* @param stockSymbol the stock to sell shares in.
- * @param player the player buying stock.
+ * @param player the player buying stock.
*
- * @return Transaction representing the transaction.
+ * @return List of transactions commited to sell the shares.
*
- * @throws IllegalArgumentException if any parameter is null, or if player does not have enough shares.
+ * @throws IllegalArgumentException if any parameter is null,
+ * or if player does not have enough shares.
* */
public List sell(BigDecimal amount,
final String stockSymbol,
final Player player)
throws IllegalArgumentException {
- if (amount == null || player == null || !Validator.NOT_EMPTY.isValid(stockSymbol)) {
+ if (amount == null
+ || player == null
+ || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) {
throw new IllegalArgumentException("Invalid sell!");
} else {
@@ -220,7 +231,8 @@ public List sell(BigDecimal amount,
.filter(s -> s.getStock().getSymbol().equals(stockSymbol))
.toList();
- BigDecimal totalOwned = player.getPortfolio().getTotalSharesBySymbol(stockSymbol);
+ BigDecimal totalOwned = player.getPortfolio()
+ .getTotalSharesBySymbol(stockSymbol);
if (amount.compareTo(totalOwned) > 0) {
amount = totalOwned;
@@ -239,7 +251,9 @@ public List sell(BigDecimal amount,
remainingToSell = remainingToSell.subtract(shareQty);
transactions.add(sell(share, player));
} else {
- Share newShare = player.getPortfolio().splitShare(share, remainingToSell);
+ Share newShare = player.getPortfolio().splitShare(
+ share, remainingToSell
+ );
remainingToSell = BigDecimal.ZERO;
transactions.add(sell(newShare, player));
}
@@ -250,12 +264,19 @@ public List sell(BigDecimal amount,
/**
* Method for advancing time, increasing the amount of weeks.
+ *
+ * Applies a random price change from -5% to 5% to every stock,
+ * plus a flat percent determined by their fortune, that can range from
+ * -10% to +10%.
+ *
+ * @see edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.GameEngineView
* */
public void advance() {
for (Stock stock : stockMap.values()) {
BigDecimal currentPrice = stock.getSalesPrice();
- double change = ((random.nextDouble() * 0.10) - 0.05) + stock.getFortune();
+ double change = (
+ (random.nextDouble() * 0.10) - 0.05) + stock.getFortune();
stock.setFortune(0);
BigDecimal factor = BigDecimal.valueOf(1 + change);
@@ -269,11 +290,17 @@ public void advance() {
* Method for getting the stocks with the most
* amount of increase since last week.
*
- * @param limit the maximum amount of stocks returned
+ * @param limit the maximum amount of stocks returned
*
* @return list of {@link Stock} objects.
+ *
+ * @throws IllegalArgumentException if limit is invalid (negative or zero).
* */
- public List getGainers(final int limit) {
+ public List getGainers(final int limit)
+ throws IllegalArgumentException {
+ if (limit < 1) {
+ throw new IllegalArgumentException("Invalid limit for getting gainers!");
+ }
return stockMap.entrySet().stream()
// We only want the stocks with a positive price change.
.filter(e ->
@@ -294,11 +321,17 @@ public List getGainers(final int limit) {
* Method for getting the stocks with the highest
* loss of price since last week.
*
- * @param limit the maximum amount of stocks returned
+ * @param limit the maximum amount of stocks returned
*
* @return list of {@link Stock} objects.
+ *
+ * @throws IllegalArgumentException if limit is invalid (negative or zero).
* */
- public List getLosers(final int limit) {
+ public List getLosers(final int limit)
+ throws IllegalArgumentException {
+ if (limit < 1) {
+ throw new IllegalArgumentException("Invalid limit for getting losers!");
+ }
return stockMap.entrySet().stream()
// Only get entries with negative price change.
.filter(e ->
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java
index 483adb8..0c7ea5e 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java
@@ -82,9 +82,9 @@ public List getShares() {
* @throws IllegalArgumentException if symbol is invalid.
*/
public List getShares(final String symbol) throws IllegalArgumentException {
- if (!Validator.VALID_STOCK_NAME.isValid(symbol)) {
+ if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) {
throw new IllegalArgumentException(
- Validator.VALID_STOCK_NAME.getErrorMessage());
+ Validator.VALID_STOCK_SYMBOL.getErrorMessage());
}
return shares.stream()
.filter(s -> symbol.equalsIgnoreCase(s.getStock().getSymbol()))
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java
index 1ca9664..0aff105 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java
@@ -44,9 +44,9 @@ public final class Stock {
public Stock(final String symbol,
final String company,
final BigDecimal salesPrice) throws IllegalArgumentException {
- if (!Validator.VALID_STOCK_NAME.isValid(symbol)) {
+ if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) {
throw new IllegalArgumentException(
- Validator.VALID_STOCK_NAME.getErrorMessage());
+ Validator.VALID_STOCK_SYMBOL.getErrorMessage());
} else {
this.symbol = symbol;
this.company = company;
diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java
index e185fb3..86838a9 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java
@@ -84,7 +84,7 @@ protected void initInteractions() {
* @return the total quantity, or 0 if the symbol is invalid.
* */
private int ownedQuantity(final String symbol) {
- if (!Validator.VALID_STOCK_NAME.isValid(symbol)) {
+ if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) {
return 0;
}
return player.getPortfolio().getShares(symbol).stream()
diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java
index 8f4f1a5..e4dd5ee 100644
--- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java
@@ -4,98 +4,183 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Player;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Share;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock;
+import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction;
import java.math.BigDecimal;
import java.util.List;
-import edu.ntnu.idi.idatt2003.g40.mappe.model.*;
+import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator;
+import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator;
+import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
final class ExchangeTest {
+ /**
+ * Test sample stock.
+ * */
private Stock appleStock;
+ /**
+ * Test sample stock.
+ * */
+ private Stock teslaStock;
+
+ /**
+ * Test sample stock.
+ * */
+ private Stock pearStock;
+
+ /**
+ * Test exchange to use.
+ * */
+ private Exchange testExchange;
+
+ /**
+ * Test player for testing transactions.
+ * */
+ private Player testPlayer;
+
@BeforeEach
void setUp() {
appleStock = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200"));
+ pearStock = new Stock("Pear", "Pear Inc.", new BigDecimal("97"));
+ testExchange = new Exchange("NASDAQ", List.of(appleStock, teslaStock));
+ testPlayer = new Player("Alice", new BigDecimal("1000"));
}
@Test
void constructorSetsNameWeekAndStocksCorrectly() {
- Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200"));
-
- Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla));
-
- assertEquals("NASDAQ", exchange.getName());
- assertEquals(1, exchange.getWeek());
- assertTrue(exchange.hasStock("AAPL"));
- assertTrue(exchange.hasStock("TSLA"));
+ assertEquals("NASDAQ", testExchange.getName());
+ assertEquals(1, testExchange.getWeek());
+ assertTrue(testExchange.hasStock("AAPL"));
+ assertTrue(testExchange.hasStock("TSLA"));
}
@Test
void getStockReturnsCorrectStock() {
- Exchange exchange = new Exchange("NASDAQ", List.of(appleStock));
-
- Stock result = exchange.getStock("AAPL");
+ Stock result = testExchange.getStock("AAPL");
assertSame(appleStock, result);
}
@Test
void findStocksReturnsMatchingStocksBySymbolOrCompany() {
- Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200"));
- Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla));
-
- List resultBySymbol = exchange.findStocks("AAP");
- List resultByCompany = exchange.findStocks("tes");
+ List resultBySymbol = testExchange.findStocks("AAP");
+ List resultByCompany = testExchange.findStocks("tes");
assertEquals(1, resultBySymbol.size());
assertTrue(resultBySymbol.contains(appleStock));
assertEquals(1, resultByCompany.size());
- assertTrue(resultByCompany.contains(tesla));
+ assertTrue(resultByCompany.contains(teslaStock));
}
@Test
void buyReturnsPurchaseAndWithdrawsMoneyFromPlayer() {
- Exchange exchange = new Exchange("NASDAQ", List.of(appleStock));
- Player player = new Player("Alice", new BigDecimal("1000"));
+ Transaction transaction =
+ testExchange.buy("AAPL", new BigDecimal("2"), testPlayer);
+
+ TransactionCalculator calculator =
+ new PurchaseCalculator(transaction.getShare());
- Transaction transaction = exchange.buy("AAPL", new BigDecimal("2"), player);
+ BigDecimal expectedPlayerMoney =
+ testPlayer.getStartingMoney().subtract(calculator.calculateTotal());
assertInstanceOf(Purchase.class, transaction);
assertEquals(1, transaction.getWeek());
assertEquals(new BigDecimal("2"), transaction.getShare().getQuantity());
- assertEquals(new BigDecimal("100"), transaction.getShare()
+ assertEquals(appleStock.getSalesPrice(), transaction.getShare()
.getPurchasePrice());
- assertEquals(new BigDecimal("799.000"), player.getMoney());
+ assertEquals(expectedPlayerMoney, testPlayer.getMoney());
}
@Test
void sellReturnsSaleAndAddsMoneyToPlayer() {
- Stock apple = new Stock("AAPL", "Apple", new BigDecimal("150"));
- Exchange exchange = new Exchange("NASDAQ", List.of(apple));
- Player player = new Player("Alice", new BigDecimal("1000"));
- Share share = new Share(apple, new BigDecimal("2"), new BigDecimal("100"));
+ Share share =
+ new Share(appleStock, new BigDecimal("2"), appleStock.getSalesPrice());
- Transaction transaction = exchange.sell(share, player);
+ Transaction transaction = testExchange.sell(share, testPlayer);
+
+ TransactionCalculator calculator = new SaleCalculator(share);
+ BigDecimal expectedPlayerMoney =
+ testPlayer.getStartingMoney().add(calculator.calculateTotal());
assertInstanceOf(Sale.class, transaction);
assertEquals(1, transaction.getWeek());
assertSame(share, transaction.getShare());
- assertEquals(new BigDecimal("1267.9000"), player.getMoney());
+ assertEquals(expectedPlayerMoney, testPlayer.getMoney());
+ }
+
+ @Test
+ void sellingAmountOfSharesRequiringSplitFunctions() {
+ Transaction purchase = testExchange.buy(
+ appleStock.getSymbol(), new BigDecimal("3"), testPlayer
+ );
+
+ List sales = testExchange.sell(
+ new BigDecimal("1.5"),
+ appleStock.getSymbol(),
+ testPlayer
+ );
+
+ SaleCalculator saleCalculator = new SaleCalculator(sales.getFirst().getShare());
+ PurchaseCalculator purchaseCalculator = new PurchaseCalculator(purchase.getShare());
+ BigDecimal expectedPlayerMoney = testPlayer.getStartingMoney()
+ .subtract(purchaseCalculator.calculateTotal())
+ .add(saleCalculator.calculateTotal());
+
+ assertEquals(1, sales.size());
+
+ assertEquals(expectedPlayerMoney, testPlayer.getMoney());
+
+ BigDecimal expectedShareAmountOwned = purchase.getShare().getQuantity()
+ .subtract(sales.getFirst().getShare().getQuantity());
+
+ assertEquals(expectedShareAmountOwned,
+ testPlayer
+ .getPortfolio()
+ .getShares(appleStock.getSymbol())
+ .getFirst()
+ .getQuantity()
+ );
+ }
+
+ @Test
+ void sellingAmountOfSharesRequiringSplitFunctions2() {
+ // Arrange: Clear setup with explicit numbers
+ Transaction purchase1 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("3.12"), testPlayer);
+ Transaction purchase2 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer);
+
+ // Act: Sell 6 shares (requires taking 3.12 from purchase1 and 2.88 from purchase2)
+ List sales = testExchange.sell(new BigDecimal("6.00"), appleStock.getSymbol(), testPlayer);
+
+ // Assert 1: Verify the split logic actually happened
+ assertEquals(2, sales.size(), "Should split the sale across two purchase records");
+ assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity());
+ assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity());
+
+ // Assert 2: Verify player money matches a known, pre-calculated constant
+ BigDecimal expectedFinalMoney = new BigDecimal("822.16000"); // Replace with the actual hardcoded math result
+ assertEquals(expectedFinalMoney, testPlayer.getMoney());
}
@Test
void advanceIncreasesWeekAndUpdatesStockPrice() {
- Exchange exchange = new Exchange("NASDAQ", List.of(appleStock));
BigDecimal oldPrice = appleStock.getSalesPrice();
- exchange.advance();
+ testExchange.advance();
- assertEquals(2, exchange.getWeek());
+ assertEquals(2, testExchange.getWeek());
assertNotEquals(oldPrice, appleStock.getSalesPrice());
}
@@ -146,4 +231,20 @@ void getLosersActuallyReturnsProperLosers() {
boolean actualLosersNotContainingLoserOutsideOfLimit = !actualLosers.contains(pearStock);
assertTrue(actualLosersNotContainingLoserOutsideOfLimit);
}
+
+ @Test
+ void invalidLimitForGettingGainersThrowError() {
+ Exchange exchange = new Exchange("Exchange", List.of(appleStock));
+
+ assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(0));
+ assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-1));
+ }
+
+ @Test
+ void invalidLimitForGettingLosersThrowError() {
+ Exchange exchange = new Exchange("Exchange", List.of(appleStock));
+
+ assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(0));
+ assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1));
+ }
}