diff --git a/millions/millions.iml b/millions/millions.iml index f23133a..abe781f 100644 --- a/millions/millions.iml +++ b/millions/millions.iml @@ -5,7 +5,4 @@ - - \ No newline at end of file diff --git a/millions/pom.xml b/millions/pom.xml index bd65466..117b7d9 100644 --- a/millions/pom.xml +++ b/millions/pom.xml @@ -26,7 +26,7 @@ org.openjfx javafx-controls - 25.0.2 + 25.0.1 @@ -46,6 +46,9 @@ org.openjfx javafx-maven-plugin 0.0.8 + + no.ntnu.gruppe53.App + org.apache.maven.plugins diff --git a/millions/src/main/java/no/ntnu/gruppe53/App.java b/millions/src/main/java/no/ntnu/gruppe53/App.java index 4205122..3170ab6 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/App.java +++ b/millions/src/main/java/no/ntnu/gruppe53/App.java @@ -1,10 +1,41 @@ package no.ntnu.gruppe53; +import javafx.application.Application; +import javafx.stage.Stage; +import no.ntnu.gruppe53.controller.StartViewController; +import no.ntnu.gruppe53.controller.ViewController; +import no.ntnu.gruppe53.view.StartView; + /** - * Hello world! + * Represents the main app of the application. + * + *

Sets up necessary components and shows the first view + * of the application: {@link StartView}.

*/ -public class App { - public static void main(String[] args) { - System.out.println("Hello World!"); - } +public class App extends Application { + @Override + public void start(Stage stage) { + ViewController vm = new ViewController(stage); + + StartView startView = new StartView(vm); + + new StartViewController(startView, vm); + + + vm.addView("start", startView); + + vm.switchView("start"); + + stage.setTitle("Millions - the Stock Game"); + stage.show(); + } + + /** + * Launches the program. + * + * @param args arguments + */ + static void main(String[] args) { + launch(); + } } diff --git a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/Exchange.java deleted file mode 100644 index 0f729a5..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java +++ /dev/null @@ -1,190 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.*; -import java.util.stream.Collectors; - -public class Exchange { - private final String name; - private int week; - private final Map stockMap; - private final Random random; - - public Exchange(String name, List stocks) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Exchange name cannot be null or blank"); - } - - if (stocks == null) { - throw new IllegalArgumentException("Stock list cannot be null"); - } - - this.name = name; - this.week = 1; - this.stockMap = new HashMap<>(); - this.random = new Random(); - - for (Stock stock : stocks) { - if (stock == null) { - throw new IllegalArgumentException("Stock cannot be null"); - } - - if (stockMap.containsKey(stock.getSymbol())) { - throw new IllegalArgumentException("Duplicate stock symbol: " + stock.getSymbol()); - } - - stockMap.put(stock.getSymbol(), stock); - } - - } - - public String getName() { - return this.name; - } - - public int getWeek() { - return this.week; - } - - - // Returns Boolean value of if the exchange has the stock. - public boolean hasStock(String symbol) { - - return stockMap.containsKey(symbol); - } - - // Returns stock that corresponds to given symbol - public Stock getStock(String symbol) { - if (symbol == null || !stockMap.containsKey(symbol)) { - throw new IllegalArgumentException("Stock not found: " + symbol); - } - return stockMap.get(symbol); - } - - public List findStocks(String searchTerm) { - //Wait for merging of other branches before implementation - if (searchTerm == null) { - return Collections.emptyList(); - } - - String search = searchTerm.toLowerCase(); - - return stockMap.values().stream().filter(stock -> - stock.getSymbol().toLowerCase().contains(search) || - stock.getCompany().toLowerCase().contains(search)).collect(Collectors.toList()); - - } - - public Transaction buy(String symbol, BigDecimal quantity, Player player) { - //Wait for merging of other branches before implementation - - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - - if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalArgumentException("Quantity must be positive"); - } - - Stock stock = getStock(symbol); - - Share share = new Share(stock, quantity, stock.getSalesPrice()); - - Transaction transaction = new Purchase(share, week); - - transaction.commit(player); - - return transaction; - - } - - public Transaction sell(Share share, Player player) { - //Wait for merging of other branches before implementation - - - - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - - Transaction transaction = new Sale(share, week); - - transaction.commit(player); - - return transaction; - } - - public void advance() { - this.week++; - - double negativePriceChange = -0.05; - double positivePriceChange = 0.1; - - for (Stock stock : stockMap.values()) { - - BigDecimal currentPrice = stock.getSalesPrice(); - - // Bruh java gjør random shit rart as - double randomPriceMultiplier = negativePriceChange + - (positivePriceChange * random.nextDouble()); - - BigDecimal bdRandomPriceMultiplier = BigDecimal.valueOf(1 + randomPriceMultiplier); - - BigDecimal changedPrice = currentPrice.multiply(bdRandomPriceMultiplier) - .setScale(2, RoundingMode.HALF_UP); - - if (changedPrice.compareTo(BigDecimal.ZERO) <= 0) { - changedPrice = new BigDecimal(1); - } - - stock.addNewSalesPrice(changedPrice); - } - } - - /** - * Lists the given amount of stocks sorted by highest increase in sales price since last week. - * @param entries amount of stocks to be shown in list - * @return a descending list of stocks with the highest price increase since last week - * @throws IllegalArgumentException if number of entries in list is negative - */ - public List getGainers(int entries) { - if (entries < 0) { - throw new IllegalArgumentException("Amount of entries in list cannot be negative."); - } - - return stockMap.values().stream() - .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed()) - .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) > 0) - .limit(entries) - - .collect(Collectors.toList()); - } - - /** - * Lists the given amount of stocks sorted by the stocks with the lowest price increase (or - * highest price decrease) - * @param entries amount of stocks to be shown in the list - * @return a descending list of stocks with the highest price decrease or - * lowest price increase - * @throws IllegalArgumentException if number of entries in list is negative - */ - public List getLosers(int entries) { - if (entries < 0) { - throw new IllegalArgumentException("Amount of entries in list cannot be negative."); - } - - return stockMap.values().stream() - .sorted(Comparator.comparing(Stock::getLatestPriceChange)) - .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) < 0) - .limit(entries) - - .collect(Collectors.toList()); - } - - -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java deleted file mode 100644 index 8133e72..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java +++ /dev/null @@ -1,128 +0,0 @@ -package no.ntnu.gruppe53; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -/** - * Handles file operations for the application. - */ - -public class FileHandler { - - /** - * Writes a list of {@link Stock}s to a text-file. Class is structed for {@code .csv} files. - * - * @param stockList the list of stocks to be converted to text - * @param path output path for the file - * @param filename name of the file (not including extension) - */ - public static void writeStocksToFile(List stockList, String path, String filename) { - String pathString = path + filename; - Path filePath = Paths.get(pathString); - - try (BufferedWriter writer = Files.newBufferedWriter(filePath, Charset.defaultCharset())) { - if (!Files.exists(filePath)) { - Files.createDirectories(filePath); - } - - writer.write("#Ticker,Name,Price"); - writer.newLine(); - writer.newLine(); - - for (Stock stock : stockList) { - String line = String.join(",", stock.getSymbol(), stock.getCompany(), - stock.getSalesPrice().toString()); - writer.write(line); - writer.newLine(); - } - - } catch (IOException e) { - System.out.println("ERROR: Something went wrong while writing the csv file."); - System.out.println("Message: " + e.getMessage()); - } - } - - /** - * Reads a .csv file and converts it to a list of {@code Stock} objects. - * - * @param path path to the file to be read - * @param filename name of the file to be read (including extension) - * @return a list of {@code Stock} objects converted from text - */ - public static List readStocksFromFile(String path, String filename) { - List stocks = new ArrayList<>(); - String pathString = path + filename; - Path filePath = Paths.get(pathString); - - try (BufferedReader reader = Files.newBufferedReader(filePath, Charset.defaultCharset())) { - String line; - String symbol; - String name; - BigDecimal price; - - while ((line = reader.readLine()) != null) { - symbol = null; - name = null; - price = null; - int counter = 0; - String trimValue = line.trim(); - - if (trimValue.isBlank() || (line.charAt(0) == '#')) { - continue; - } - - String[] values = line.split(","); - - if (values.length != 3) { - System.out.println("ERROR: Bad line in csv: "); - System.out.print(line); - System.out.println("There should be 3 values total: " + - "symbol, name, price."); - continue; - } - - for (String value : values) { - trimValue = value.trim(); - - switch (counter) { - case 0: - symbol = trimValue; - break; - case 1: - name = trimValue; - break; - case 2: - try { - price = new BigDecimal(trimValue); - } - catch (NumberFormatException e) { - System.out.println("ERROR: Not a number. Last column in .csv file " + - "needs to be the price of the stock (number). Skipping..."); - price = null; - break; - } - } - counter++; - } - if (symbol == null || symbol.isBlank() || name == null || name.isBlank() || - price == null || price.compareTo(BigDecimal.ZERO) <= 0) { - continue; - } - stocks.add(new Stock(symbol, name, price)); - } - } - catch (IOException e) { - System.out.println("ERROR: Something went wrong while reading the csv file."); - System.out.println("Message: " + e.getMessage()); - } - return stocks; - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Player.java b/millions/src/main/java/no/ntnu/gruppe53/Player.java deleted file mode 100644 index f10e8a3..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Player.java +++ /dev/null @@ -1,165 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; -import java.math.RoundingMode; - -/** - * Represents a participant in the trading game. - *

- * A {@code Player} has a name, a monetary balance, a {@link Portfolio} - * of owned {@link Share}s, and a {@link TransactionArchive} containing - * a history of the player's transactions. - *

- * - *

- * The player starts with an initial amount of money and may gain or - * lose funds through transactions. - *

- */ - -public class Player { - private final String name; - private final BigDecimal startingMoney; - private BigDecimal money; - private final Portfolio portfolio; - private final TransactionArchive transactionArchive; - - /** - * Creates a player with a given starting balance. - * - * @param name the player's name; must not be {@code null} or blank - * @param startingMoney the initial monetary balance of the - * player; must not be {@code null} or negative - * @throws IllegalArgumentException if {@code name} is {@code null} or - * blank, or if {@code startingMoney} - * is {@code null}, negative, or 0 - */ - - public Player(String name, BigDecimal startingMoney) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Player name must not be " + - "null or empty."); - } - - if (startingMoney == null || startingMoney.signum() <= 0 ) { - throw new IllegalArgumentException("Starting money must not be " + - "null, negative, nor 0."); - } - this.name = name; - this.startingMoney = startingMoney; - this.money = startingMoney; - this.portfolio = new Portfolio(); - this.transactionArchive = new TransactionArchive(); - } - - /** - * Returns the full name of the player. - * - * @return the player's name - */ - - public String getName() { - return this.name; - } - - /** - * Returns the player's current monetary balance. - * - * @return the player's current balance. - */ - - public BigDecimal getMoney() { - return this.money; - } - - /** - * Increases the players monetary balance by the given amount. - *

- * Money added must be a {@code positive} number. - *

- * - * @param amount the amount to add; must be positive and not {@code null} - * @throws IllegalArgumentException if {@code amount] is {@code null} - * or not positive - */ - - public void addMoney(BigDecimal amount) { - if (amount == null) { - throw new IllegalArgumentException("Amount must not be null."); - } - - if (amount.signum() <= 0) { - throw new IllegalArgumentException("Amount must be positive."); - } - - this.money = this.money.add(amount); - } - - /** - * Decreases the player's monetary balance by the given amount. - *

- * The player's monetary balance must not decrease below {@code 0} - * (overdrawn not possible). - *

- * - * @param amount the amount to subtract; must not be negative or {@code null} - * @throws IllegalArgumentException if the amount is not positive, {@code null} - * , or insufficient funds are available - */ - - public void withdrawMoney(BigDecimal amount) { - if (amount == null) { - throw new IllegalArgumentException("Amount must not be null."); - } - - if (amount.signum() <= 0) { - throw new IllegalArgumentException("Amount must be positive."); - } - - if (this.money.subtract(amount).signum() < 0) { - throw new IllegalArgumentException("Insufficient funds."); - } - - this.money = this.money.subtract(amount); - } - - /** - * Return's the player's portfolio of shares. - * - * @return the player's portfolio - */ - - public Portfolio getPortfolio() { - return this.portfolio; - } - - /** - * Returns the archive of the player's transaction history. - * - * @return the player's transaction archive - */ - - public TransactionArchive getTransactionArchive() { - return this.transactionArchive; - } - - public BigDecimal getNetWorth() { - return this.money.add(this.portfolio.getNetWorth()); - } - - public String getStatus(Exchange exchange) { - String status = "Novice"; - - if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("1.20")) - >= 0) && exchange.getWeek() >= 10) { - status = "Investor"; - } - - if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("2")) - >= 0) && exchange.getWeek() >= 20) { - status = "Speculator"; - } - - return status; - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java b/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java deleted file mode 100644 index 2f40774..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java +++ /dev/null @@ -1,145 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents the portfolio of the Player, containing information - * about which {@link Share}s are owned. - *

- * A {@code Portfolio} manages a mutable collection of shares, allowing - * shares to be added, removed, and queried. - *

- * - *

- * Shares are associated with unique Transaction, however, multiple - * shares from the same stock can exist in the portfolio. - *

- */ - -public class Portfolio { - private final List shares; - - /** - * Creates an empty portfolio for the player. - */ - - public Portfolio() { - this.shares = new ArrayList<>(); - } - - /** - * Adds a share to the player's portfolio. - *

- * The {@link Stock} must not be {@code null} and the share quantity - * and purchase price must be above zero. - *

- * - * @param share the share owned by the player; must not be {@code null} - * @return {@code true} if the share was added successfully, - * {@code false} otherwise - * @throws IllegalArgumentException if {@code share} is {@code null} - */ - - public boolean addShare(Share share) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - - this.shares.add(share); - - return this.getShares().getLast().equals(share); - } - - /** - * Removes a share from the player's portfolio. - *

- * The list must already exist in the portfolio to be successfully - * removed. - *

- * - * @param share the share being removed; must not be {@code null} - * @return {@code true} if the share removal was successful, - * {@code false} otherwise. - * @throws IllegalArgumentException if {@code share} is {@code null} - */ - public boolean removeShare(Share share) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - - for (Share s : this.shares) { - if (s.equals(share)) { - this.shares.remove(s); - return true; - } - } - return false; - } - - /** - * Returns a list of the shares currently in the player's portfolio. - * - * @return a list of the shares in this portfolio - */ - public List getShares() { - return this.shares; - } - - /** - * Returns a list of the shares that belongs to the stock with the - * given ticker symbol. - * - * @param symbol the ticker symbol of the stock to search for; must not be - * {@code null} or blank - * @return a list of the player's shares in the stock with the given - * stock symbol; an empty list if none are found - * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank - */ - public List getShares(String symbol) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - - var tempSymbols = new ArrayList(); - for (Share s : this.shares) { - if (s.getStock().getSymbol().equals(symbol)) { - tempSymbols.add(s); - } - } - return tempSymbols; - } - - /** - * Determines whether the player's portfolio contains the specified - * share. - * - * @param share the share to search for in this portfolio - * @return {@code true} if the player's portfolio contains the share - * {@code false} otherwise - */ - - public boolean contains(Share share) { - for (Share s : this.shares) { - if (s.equals(share)) { - return true; - } - } - return false; - } - - /** - * Calculates the net total value of the shares in the portfolio. - *

Calculations are done using a {@link SaleCalculator}.

- * - * @return the total sale value of the portfolio - */ - public BigDecimal getNetWorth() { - - return this.shares.stream() - .map(share -> new SaleCalculator(share).calculateTotal()) - .reduce(BigDecimal.ZERO, BigDecimal::add); - } - -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Purchase.java b/millions/src/main/java/no/ntnu/gruppe53/Purchase.java deleted file mode 100644 index 0b622e2..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Purchase.java +++ /dev/null @@ -1,62 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; - -/** - * Represents a purchase {@link Transaction} where a {@link Player} purchases - * a {@link Share} - *

- * A purchase can only be commited if: - *

    - *
  • The player has sufficient funds.
  • - *
  • The transaction has not already been committed.
  • - *
- *

- */ - -public class Purchase extends Transaction{ - - /** - * Constructs a purchase transaction for the given share and week. - * - * @param share the share to be purchased; must not be {@code null} - * @param week the week when the purchase transaction occurs; must be 1 or greater - */ - - public Purchase(Share share, int week) { - super(share, week, new PurchaseCalculator(share)); - } - - /** - * Commits the purchase to the given player, making the - * transaction unique. - *

- * If the transaction is not already committed, and the player - * has sufficient funds, the transaction will: - *

    - *
  • Withdraw money from the player
  • - *
  • Add the share to the player's {@link Portfolio}
  • - *
  • Archive the transaction in the player's - * {@link TransactionArchive}
  • - *
- *

- * @param player the player performing the transaction; must not be {@code null} - * @throws IllegalArgumentException if {@code player} is {@code null} - */ - - @Override - public void commit(Player player) { - if (player == null) { - throw new IllegalArgumentException("Player cannot be null."); - } - - if (!this.isCommitted() && ((player.getMoney().subtract(this.getCalculator().calculateTotal())) - .compareTo(BigDecimal.ZERO) >= 0)) { - - player.withdrawMoney(this.getCalculator().calculateTotal()); - player.getPortfolio().addShare(this.getShare()); - player.getTransactionArchive().add(this); - super.commit(player); - } - } -} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java deleted file mode 100644 index 8620cb5..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java +++ /dev/null @@ -1,69 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; - -/* -PurchaseCalculator class: - -Implements from TransactionCalculator - -Which contains: -1. Constructor which passes the values of a Share object - to its local variables. -2. calculateGross() function to calculate the gross value -3. calculateCommission() function to calculate the commission value -4. calculateTax() function which does nothing as there is no tax -5. calculateTotal() function which calculates the total purchase price - by adding all the calculations together. - - */ -public class PurchaseCalculator implements TransactionCalculator{ - private final BigDecimal purchasePrice; - private final BigDecimal quantity; - - - //Takes a Share object to pass its Purchase and Quantity Values to - //Its local private variables - public PurchaseCalculator(Share share) { - - this.purchasePrice = share.getPurchasePrice(); - this.quantity = share.getQuantity(); - - } - - // Multiplies quantity with the purchase price to get the gross value - @Override - public BigDecimal calculateGross(){ - return this.quantity.multiply(this.purchasePrice); - } - - //Takes the gross value calculated in calculateGross() - //And multiplies it with the 0.05% commission on purchase to - //get the total value of the commission - @Override - public BigDecimal calculateCommission() { - //0.5% commission on purchase - BigDecimal commission = new BigDecimal("0.005"); - - return calculateGross().multiply(commission); - } - - //No tax on purchase - @Override - public BigDecimal calculateTax() { - return new BigDecimal(0); - } - - - //Calculates the total purchase price by adding - // Gross + Commission + Tax - @Override - public BigDecimal calculateTotal() { - - return calculateGross().add(calculateCommission()).add(calculateTax()); - } - - - - -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/Sale.java deleted file mode 100644 index 00787d0..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Sale.java +++ /dev/null @@ -1,57 +0,0 @@ -package no.ntnu.gruppe53; - -/** - * Represents a sale {@link Transaction} where a {@link Player} sells - * a {@link Share} - *

- * A sale can only be commited if: - *

    - *
  • The share is in the players {@link Portfolio}
  • - *
  • The transaction has not already committed
  • - *
- *

- */ - -public class Sale extends Transaction{ - - /** - * Constructs a sale transaction for the given share and week. - * - * @param share the share to be sold; must not be {@code null} - * @param week the week when the sale transaction occurs; must be greater than 0 - */ - - public Sale(Share share, int week) { - super(share, week, new SaleCalculator(share)); - } - - /** - * Commits the sale to the given player, making the - * transaction unique. - *

- * If the player ows the share, the transaction will: - *

    - *
  • Add money to the player
  • - *
  • Remove the share from the player's portfolio
  • - *
  • Archive the transaction in the player's {@link TransactionArchive}
  • - *
- *

- * @param player the player performing the transaction; must not be {@code null} - * @throws IllegalArgumentException if {@code player} is {@code null} - */ - - @Override - public void commit(Player player) { - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - - if (!this.isCommitted() && (player.getPortfolio().contains(this.getShare())) - ) { - player.addMoney(this.getCalculator().calculateTotal()); - player.getPortfolio().removeShare(this.getShare()); - player.getTransactionArchive().add(this); - super.commit(player); - } - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java deleted file mode 100644 index b3b8345..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java +++ /dev/null @@ -1,80 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; - -/* -SaleCalculator class: - -Implements from Interface TransactionCalculator - -Which contains: -1. Constructor which passes the values of a Share object - to its local variables. -2. calculateGross() function to calculate the gross value -3. calculateCommision() function to calculate the commission value -4. calculateTax() function which calculates the 30% tax on winnings -5. calculateTotal() function which calculates the total sales price - by subtracting commission and tax from the gross value - - */ - - - -public class SaleCalculator implements TransactionCalculator{ - private final BigDecimal purchasePrice; - private final BigDecimal salesPrice; - private final BigDecimal quantity; - - //Takes a Share object to pass its Quantity, Purchase- and SalesPrice Values to - //Its local private variables - public SaleCalculator(Share share) { - this.purchasePrice = share.getPurchasePrice(); - this.quantity = share.getQuantity(); - this.salesPrice = share.getStock().getSalesPrice(); - } - - - //Multiplies quntity with the sales price to get the gross value - @Override - public BigDecimal calculateGross() { - - return this.quantity.multiply(this.salesPrice); - } - - //Takes the gross value calculated in calculateGross() - //And multiplies it with the 0.1% commission on sale to - //get the total value of the commission - @Override - public BigDecimal calculateCommission() { - - //0.1% commission - BigDecimal commision = new BigDecimal("0.01"); - - return calculateGross().multiply(commision); - } - - - //Calculates the tax on winnings, winnings are defined as - //Gross - commission - (purchaseprice * quantity) - //Tax is therefore calculated as winnings * tax(30%) - @Override - public BigDecimal calculateTax() { - - - BigDecimal tax = new BigDecimal("0.30"); - - // (Gross - commission - (Purchase price - quantity)) * 30% tax - return calculateGross().subtract(calculateCommission()) - .subtract(this.purchasePrice.multiply(this.quantity)).multiply(tax); - - } - - //Calculates the total sales price by subtracting - //Gross - Commission - Tax - @Override - public BigDecimal calculateTotal() { - - return calculateGross().subtract(calculateCommission()).subtract(calculateTax()); - - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Share.java b/millions/src/main/java/no/ntnu/gruppe53/Share.java deleted file mode 100644 index 6e3e21e..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Share.java +++ /dev/null @@ -1,75 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; - -/** - * Represents a holding of a specific {@link Stock}. - *

- * A {@code Share} contains the quantity of shares owned and - * the price per share at the time of purchase. Future price - * fluctuations are not included in share. - *

- */ - -public class Share { - private final Stock stock; - private final BigDecimal quantity; - private final BigDecimal purchasePrice; - - /** - * Creates a share for a given stock. - * - * @param stock the stock associated with the share; must not be {@code null} - * @param quantity the number of shares purchased; must not be {@code null}, negative, or zero - * @param purchasePrice price per share at the time of purchase; must not be {@code null}, negative or zero - * @throws IllegalArgumentException if {@code stock} is {@code null}, or {@code quantity} is {@code null}, - * negative, or zero, or {@code purchasePrice} is {@code null}, negative, - * or zero - */ - - public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { - if (stock == null) { - throw new IllegalArgumentException("Stock cannot be null."); - } - - if (quantity == null || quantity.signum() <= 0) { - throw new IllegalArgumentException("Quantity cannot be null, negative, or zero."); - } - - if (purchasePrice == null || purchasePrice.signum() <= 0) { - throw new IllegalArgumentException("PurchasePrice cannot be null, negative, or zero."); - } - - this.stock = stock; - this.quantity = quantity; - this.purchasePrice = purchasePrice; - } - - /** - * Returns the stock the share is associated with. - * - * @return the stock - */ - public Stock getStock() { - return stock; - } - - /** - * Returns the quantity of shares owned in the stock. - * - * @return the quantity of shares - */ - - public BigDecimal getQuantity() { - return quantity; - } - - /** - * Returns the purchase price per share for the stock at time of purchase. - * - * @return the purchase price of the shares. - */ - public BigDecimal getPurchasePrice() { - return purchasePrice; - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Stock.java b/millions/src/main/java/no/ntnu/gruppe53/Stock.java deleted file mode 100644 index 71d93f4..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Stock.java +++ /dev/null @@ -1,148 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Represents a stock with its ticker symbol, company name, - * and a list of sales prices. - *

- * The list is a historical representation of price changes - * over time for the given stock. - *

- *

- * The most recently added price represents the current market price. - *

- * - *

- * A {@code Stock} instance always contains at least one price, - * instantiated at construction. - *

- */ - -public class Stock { - private final String symbol; - private final String company; - private final List prices; - - /** - * Creates a stock with an already defined sales price. - * - * @param symbol the stock's unique ticker symbol (e.g. "AAPL"); must not be {@code null} or blank - * @param company the full name of the company; must not be {@code null} or blank - * @param salesPrice the current price per share; must not be {@code null}, negative, or zero - * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank, {@code company} is - * {@code null} or blank, or {@code salesPrice} is {@code null}, - * negative, or zero - */ - public Stock(String symbol, String company, BigDecimal salesPrice) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or empty."); - } - - if (company == null || company.isBlank()) { - throw new IllegalArgumentException("Company cannot be null or empty."); - } - - if (salesPrice == null || salesPrice.signum() <= 0) { - throw new IllegalArgumentException("SalesPrice cannot be null, negative, or zero."); - } - - this.symbol = symbol; - this.company = company; - this.prices = new ArrayList<>(); - this.prices.add(salesPrice); - } - - /** - * Returns the stock's unique ticker symbol. - * - * @return the stock's ticker symbol - */ - public String getSymbol() { - return this.symbol; - } - - /** - * Returns the full name of the company associated with the stock. - * - * @return the full name of the stock's company - */ - public String getCompany() { - return this.company; - } - - /** - * Returns the current sale price per share of the stock. - *

- * The current price is defined as the most recent price in the price history. - *

- * @return the current sale price of the stock - */ - public BigDecimal getSalesPrice() { - return this.prices.get(prices.size() - 1); - } - - /** - * Adds a new sales price to the stock's price history. - *

- * The added price becomes the new current market price. - *

- * - *

- * The new price must be positive. - *

- * @param price the new market price per share of the stock; must not be {@code null}, negative or zero - * @throws IllegalArgumentException if {@code price} is {@code null}, negative, or zero - */ - public void addNewSalesPrice(BigDecimal price) { - if (price == null || price.signum() <= 0) { - throw new IllegalArgumentException("Price cannot be null, negative, or zero."); - } - this.prices.add(price); - } - - /** - * Returns a list of all the sales prices for this stock since (and including) week 1. - * - * @return a list of all the stock's sales prices - */ - public List getHistoricalPrices() { - return this.prices; - } - - /** - * Returns the maximum historical sales price since (and including) week 1 for the stock. - * - * @return the stock's highest sales price - */ - public BigDecimal getHighestPrice() { - return Collections.max(this.prices); - } - - /** - * Returns the minimum historical sales price since (and including) week 1 for the stock. - * - * @return the stock's lowest sales price - */ - public BigDecimal getLowestPrice() { - return Collections.min(this.prices); - } - - /** - * Returns the sales price increase/decrease from previous week to this week. Returns 0 if - * the price list only has 1 value. - * - * @return the net difference in sales price between the last and second last week - */ - public BigDecimal getLatestPriceChange() { - if (!(this.prices.size() < 2)) { - return this.prices.getLast().subtract(this.prices.get(this.prices.size() - 2)); - } - else { - return BigDecimal.ZERO; - } - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/Transaction.java b/millions/src/main/java/no/ntnu/gruppe53/Transaction.java deleted file mode 100644 index 5ba478c..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/Transaction.java +++ /dev/null @@ -1,99 +0,0 @@ -package no.ntnu.gruppe53; - -/** - * Represents a financial transaction of a {@link Share}. - *

- * A transaction occurs in a specific week and uses a - * {@link TransactionCalculator} to determine the monetary change. - * Transactions are unique and is committed to a {@link Player}'s - * {@link Portfolio}. - *

- * - *

- * Subclasses define the nature of the transaction (e.g. purchase - * or sale) by implementing the {@link #commit(Player)} - *

- */ - -abstract public class Transaction { - private final Share share; - private final int week; - private final TransactionCalculator calculator; - protected boolean committed; - - /** - * Constructor for a transaction. - * - * @param share the share for the transaction; must not be {@code null} - * @param week the week the transaction takes place; must be greater than 0 - * @param calculator the transaction calculator used to - * compute the {@link Transaction} - * @throws IllegalArgumentException if {@code share} is {@code null} or {@code week} - * is less than 1 - */ - - protected Transaction(Share share, int week, TransactionCalculator calculator) { - if (week <= 0) { - throw new IllegalArgumentException("Week cannot be negative or zero"); - } - - this.share = share; - this.week = week; - this.calculator = calculator; - this.committed = false; - } - - /** - * Returns the share for the current transaction. - * - * @return the share belonging to the transaction - */ - public Share getShare() { - return this.share; - } - - /** - * Returns the week the transaction took place. - * - * @return the week for the transaction - */ - public int getWeek() { - return this.week; - } - - /** - * Returns the transaction calculator used to calculate - * the transaction. - * - * @return the transaction calculator used for the transaction - */ - - public TransactionCalculator getCalculator() { - return this.calculator; - } - - /** - * Returns whether the transaction has been committed to a player. - * - * @return {@code true} if committed, {@code false} otherwise - */ - - public boolean isCommitted() { - return this.committed; - } - - /** - * Commits the transaction to a player, making it unique. - * - *

- * Subclasses should define the business logic required before - * committing. Then {@code super.commit(player)} should be called - * once the transaction has been successfully applied. - *

- * @param player the player performing the transaction; must not be {@code null} - */ - - public void commit(Player player) { - this.committed = true; - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java b/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java deleted file mode 100644 index a6ca20d..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java +++ /dev/null @@ -1,136 +0,0 @@ -package no.ntnu.gruppe53; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -/** - * Maintains a history of all completed {@link Transaction}s. - *

- * A {@code TransactionArchive} stores transactions in the order - * they are added and provides methods to retrieve transactions - * filtered by type and week. - *

- */ - -public class TransactionArchive { - private final List transactions; - - /** - * Creates an empty transaction archive. - */ - - public TransactionArchive() { - this.transactions = new ArrayList<>(); - } - - /** - * Adds a transaction to the archive. - * - * @param transaction the transaction being added; must not be {@code null} - * @return {@code true} if the transaction was successfully added - * {@code false} otherwise - * @throws IllegalArgumentException if {@code transaction} is {@code null} - */ - - public boolean add(Transaction transaction) { - if (transaction == null) { - throw new IllegalArgumentException("Transaction cannot be null."); - } - - this.transactions.add(transaction); - - return this.getTransactions().get(transactions.size()-1).equals(transaction); - } - - /** - * Checks whether there are no recorded transactions in the archive. - * - * @return {@code true} if the archive is empty. - * {@code false} otherwise - */ - - public boolean isEmpty() { - return this.transactions.isEmpty(); - } - - /** - * Returns all the transactions contained in the archive. - * - * @return the list of transactions - */ - - public List getTransactions() { - return this.transactions; - } - - /** - * Returns all {@link Purchase} transactions for the given week. - * - * @param week the week number for transactions; must be greater than 0 - * @return a list of purchase transactions for the given week; - * empty if none exist - * @throws IllegalArgumentException if {@code week} is not greater than 0 - */ - - public List getPurchases(int week) { - if (week <= 0) { - throw new IllegalArgumentException("Week must be greater than 0."); - } - var purchaseList = new ArrayList(); - - for (Transaction t : this.transactions) { - if (t instanceof Purchase && t.getWeek() == week) { - purchaseList.add((Purchase) t); - } - } - return purchaseList; - } - - /** - * Returns all {@link Sale} transactions for the given week. - * - * @param week the week number for transactions; must be greater than 0 - * @return a list of sale transactions for the given week; - * empty if none exist - * @throws IllegalArgumentException if {@code week} is not greater than 0 - */ - - public List getSales(int week) { - if (week <= 0) { - throw new IllegalArgumentException("Week must be greater than 0."); - } - - var saleList = new ArrayList(); - - for (Transaction t : this.transactions) { - if (t instanceof Sale && t.getWeek() == week) { - saleList.add((Sale) t); - } - } - return saleList; - } - - /** - * Counts the number of distinct week numbers represented in the - * transaction archive. - *

- * A week is considered distinct if at least one transaction has - * occurred within that week. Multiple transactions in the span of the - * same week will only be counted as one distinct week. - *

- * - * @return the number of unique week values in the archive; - * {@code 0} if the archive contains no transactions - */ - - public int countDistinctWeeks() { - var distinctWeeks = new HashSet(); - - for (Transaction t : this.transactions) { - distinctWeeks.add(t.getWeek()); - } - - return distinctWeeks.size(); - } -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java deleted file mode 100644 index 0ad9274..0000000 --- a/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java +++ /dev/null @@ -1,11 +0,0 @@ -package no.ntnu.gruppe53; - -import java.math.BigDecimal; - -public interface TransactionCalculator { - BigDecimal calculateGross(); - BigDecimal calculateCommission(); - BigDecimal calculateTax(); - BigDecimal calculateTotal(); - -} diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java new file mode 100644 index 0000000..0514822 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java @@ -0,0 +1,377 @@ +package no.ntnu.gruppe53.controller; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.stage.Stage; +import javafx.util.Subscription; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.model.PurchaseCalculator; +import no.ntnu.gruppe53.model.SaleCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; +import no.ntnu.gruppe53.service.LanguageManager; +import no.ntnu.gruppe53.service.StockLineChartService; +import no.ntnu.gruppe53.view.EndView; +import no.ntnu.gruppe53.view.FooterBar; +import no.ntnu.gruppe53.view.MarketView; +import no.ntnu.gruppe53.view.NavigationBar; +import no.ntnu.gruppe53.view.PortfolioView; +import no.ntnu.gruppe53.view.TransactionArchiveView; + +/** + * Represents the main controller for the views of the stock game. + * + *

Uses a {@link LanguageManager} for methods to change text dynamically.

+ */ +public class GameController { + private Player player; + private Exchange exchange; + private final LanguageManager lm = LanguageManager.getInstance(); + + private final ViewController vm; + + private MarketView marketView; + private PortfolioView portfolioView; + private TransactionArchiveView transactionArchiveView; + private EndView endView; + + private Subscription selectedStockPriceSubscription; + private Subscription selectedSharePriceSubscription; + + /** + * Initializes the {@link ViewController} to be used for switching views. + * + * @param vm {@code ViewController} to be used when switching views + */ + public GameController(ViewController vm) { + this.vm = vm; + } + + /** + * Starts a new game by initializing all main views and navigation. + * + *

Creates the player and exchange to be used by the game from + * data from the {@link GameSetupController}.

+ */ + public void startNewGame() { + Stage primaryStage = vm.getStage(); + + GameSetupController setupController = new GameSetupController(primaryStage); + GameSetupController.SetupResult setupResult = setupController.setupNewGame(); + + if (setupResult == null) { + return; + } + + player = setupResult.getPlayer(); + List stocks = setupResult.getStocks(); + + exchange = new Exchange("Millions Exchange", stocks); + + NavigationBar sharedNav = new NavigationBar(); + FooterBar sharedFooter = new FooterBar(); + vm.setGlobalPanels(sharedNav, sharedFooter); + + marketView = new MarketView(); + // marketView.setPlayer(player); + marketView.setExchange(exchange); + + portfolioView = new PortfolioView(); + portfolioView.setPlayer(player); + + transactionArchiveView = new TransactionArchiveView(); + transactionArchiveView.setPlayer(player); + + endView = new EndView(); + + sharedNav.bindPlayer(player); + sharedNav.bindExchange(exchange); + sharedFooter.setExchange(exchange); + + vm.addView("portfolio", portfolioView); + vm.addView("market", marketView); + vm.addView("history", transactionArchiveView); + vm.addView("endGame", endView); + + initialize(sharedNav, sharedFooter); + + vm.switchView("market"); + } + + /** + * Switches the view to the {@link MarketView}. + * + *

MarketView lets the player perform {@link no.ntnu.gruppe53.model.Purchase} transactions.

+ */ + public void showMarketView() { + if (marketView != null) { + vm.switchView("market"); + } + } + + /** + * Switches the view to the {@link PortfolioView}. + * + *

Contains the data from a player's {@link no.ntnu.gruppe53.model.Portfolio}.

+ * + *

PortfolioView lets theEndView player perform + * {@link no.ntnu.gruppe53.model.Sale} transactions.

+ */ + public void showPortfolio() { + if (portfolioView != null) { + vm.switchView("portfolio"); + } + } + + /** + * Switches the view to the {@link TransactionArchiveView}. + * + *

TransactionArchiveView contains data from + * {@link no.ntnu.gruppe53.model.TransactionArchive}

+ */ + public void showTransactionHistory() { + if (transactionArchiveView != null) { + vm.switchView("history"); + } + } + + /** + * Initializes the {@link NavigationBar} and {@link FooterBar} for the controller. + * + *

Binds functionality to exposed buttons and lists in views.

+ * + *

Sets subscriptions for observable values for views.

+ * + *

Sets the method for setting the selected language locale for the footer.

+ * + * @param nav the NavigationBar view + * @param footer the FooterBar view + */ + public void initialize(NavigationBar nav, FooterBar footer) { + nav.onMarketButtonClick(() -> vm.switchView("market")); + nav.onPortfolioButtonClick(() -> vm.switchView("portfolio")); + nav.onHistoryButtonClick(() -> vm.switchView("history")); + nav.onEndGameButtonClick(this::endGame); + + footer.onLanguageChange(selected -> lm.setLocale(selected.locale())); + + footer.onAdvanceButtonClick(() -> { + if (exchange != null) { + exchange.advance(); + } + }); + + marketView.onStockSelection(stock -> { + if (selectedStockPriceSubscription != null) { + selectedStockPriceSubscription.unsubscribe(); + selectedStockPriceSubscription = null; + } + + if (stock != null) { + marketView.setSelectedStockLabel(stock.getSymbol() + " - " + stock.getCompany()); + marketView.setupPriceSubscription(stock, currentStock -> { + var series = StockLineChartService.buildSeries(currentStock, exchange); + marketView.updateChart(series, currentStock.getCompany()); + }); + + marketView.setPurchaseCalculator(calculatePurchase( + marketView.getSelectedStock(), marketView.getQuantityPurchase())); + + selectedStockPriceSubscription = stock.salesPriceProperty().subscribe(newPrice -> { + if (marketView.getSelectedStock() != null) { + marketView.setPurchaseCalculator(calculatePurchase( + marketView.getSelectedStock(), + marketView.getQuantityPurchase() + )); + } + }); + + } else { + marketView.setSelectedStockLabel(""); + marketView.clearChart(); + } + }); + + marketView.onPurchaseButton(() -> { + Stock stock = marketView.getSelectedStock(); + if (stock != null) { + purchaseStock(stock.getSymbol(), marketView.getQuantityPurchase()); + } + }); + + marketView.onQuantitySelect(() -> { + if (marketView.getSelectedStock() != null) { + marketView.setPurchaseCalculator(calculatePurchase( + marketView.getSelectedStock(), marketView.getQuantityPurchase())); + } + }); + + portfolioView.onShareSelection(share -> { + if (selectedSharePriceSubscription != null) { + selectedSharePriceSubscription.unsubscribe(); + selectedSharePriceSubscription = null; + } + + if (share != null) { + portfolioView.setSelectedShareLabel( + share.getStock().getSymbol() + " - " + share.getStock().getCompany()); + portfolioView.setSaleCalculator(calculateSale(portfolioView.getSelectedShare())); + + selectedSharePriceSubscription = + share.getStock().salesPriceProperty().subscribe(newPrice -> { + + if (portfolioView.getSelectedShare() != null) { + portfolioView.setSaleCalculator( + calculateSale(portfolioView.getSelectedShare())); + } + }); + + } else { + portfolioView.setSelectedShareLabel(""); + } + }); + + portfolioView.onSellButton(this::handleSell); + + endView.onQuitButton(Platform::exit); + + endView.onNewGameButton(this::startNewGame); + } + + /** + * Lets a player purchase {@link Share}s in a stock. + * + *

Utilizes the buy method from {@link Exchange}.

+ * + * @param stockSymbol the symbol/ticker of the stock to purchase shares for + * @param quantity the quantity of shares to be purchased + */ + private void purchaseStock(String stockSymbol, int quantity) { + if (exchange == null || player == null || stockSymbol == null || quantity <= 0) { + return; + } + + try { + exchange.buy(stockSymbol, BigDecimal.valueOf(quantity), player); + } catch (IllegalStateException | IllegalArgumentException ex) { + showErrorAlert("Purchase Failed", ex.getMessage()); + } + } + + /** + * Handles sale of the selected share. + */ + private void handleSell() { + Share selectedShare = portfolioView.getSelectedShare(); + if (selectedShare != null) { + sellShare(selectedShare); + } + } + + /** + * Lets a player sell a share from their portfolio. + * + * @param selectedShare the currently selected share + */ + private void sellShare(Share selectedShare) { + if (exchange == null || player == null || selectedShare == null) { + return; + } + try { + exchange.sell(selectedShare, player); + } catch (IllegalStateException | IllegalArgumentException ex) { + showErrorAlert("Sale Failed", ex.getMessage()); + } + } + + /** + * Creates a {@link PurchaseCalculator} that can be used for the + * setPurchaseCalculator() function in the {@link MarketView}. + * + * @param stock the currently selected stock + * @param quantity the currently selected quantity + * @return {@link PurchaseCalculator} + */ + private PurchaseCalculator calculatePurchase(Stock stock, int quantity) { + stock = marketView.getSelectedStock(); + quantity = marketView.getQuantityPurchase(); + + Share share = new Share(stock, BigDecimal.valueOf(quantity), stock.getSalesPrice()); + PurchaseCalculator purchaseCalculator; + return purchaseCalculator = new PurchaseCalculator(share); + } + + /** + * Creates a {@link SaleCalculator} that can be used for the + * setSaleCalculator() function in the {@link PortfolioView}. + * + * @param share the share to use the calculator on + * @return {@link SaleCalculator} + */ + private SaleCalculator calculateSale(Share share) { + + share = portfolioView.getSelectedShare(); + + SaleCalculator saleCalculator; + return saleCalculator = new SaleCalculator(share); + } + + /** + * Creates an alert where the Player can confirm if they want to end the game. + * If yes, then sells entire portfolio and displays the EndView. + */ + private void endGame() { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.titleProperty().bind(lm.bindString("endGameAlertTitle")); + alert.headerTextProperty().bind(lm.bindString("endGameAlertHeaderText")); + alert.contentTextProperty().bind(lm.bindString("endGameAlertContentText")); + + ButtonType yesButton = new ButtonType(lm.getString("yes"), ButtonBar.ButtonData.YES); + ButtonType noButton = new ButtonType(lm.getString("no"), ButtonBar.ButtonData.NO); + + alert.getButtonTypes().setAll(yesButton, noButton); + + alert.showAndWait().ifPresent(button -> { + if (button == yesButton) { + sellPortfolio(); + vm.setGlobalPanels(null, null); + vm.switchView("endGame"); + + endView.getStatusLabel().setText(player.getStatus(exchange)); + endView.getMoneyLabel().setText( + player.getMoney().setScale(2, RoundingMode.HALF_EVEN).toString()); + } + }); + } + + /** + * Sells a player's entire portfolio. + */ + private void sellPortfolio() { + List sharesToSell = List.copyOf(player.getPortfolio().getShares()); + + for (Share share : sharesToSell) { + exchange.sell(share, player); + } + } + + /** + * Creates a default error alert to be used for notifying the player of errors. + * + * @param title title of the error alert + * @param content the message content of the error alert + */ + private void showErrorAlert(String title, String content) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(content); + alert.showAndWait(); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java new file mode 100644 index 0000000..feced72 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java @@ -0,0 +1,122 @@ +package no.ntnu.gruppe53.controller; + +import java.io.File; +import java.math.BigDecimal; +import java.util.List; +import javafx.stage.Stage; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.model.Stock; +import no.ntnu.gruppe53.service.FileHandler; +import no.ntnu.gruppe53.view.CsvChooserView; +import no.ntnu.gruppe53.view.PlayerNameChooserView; +import no.ntnu.gruppe53.view.PlayerStartingMoneyChooserView; + +/** + * Controller for setting up relevant game data to start a new game. + * + *

Lets the {@link Player} set their name, starting money + * and provide a .csv file of {@link Stock} data to populate + * the {@link Exchange}.

+ */ +public class GameSetupController { + /** + * Nested class to pass the player and stocks values over to the + * {@link GameController}. + */ + public static class SetupResult { + private final Player player; + private final List stocks; + + /** + * Initializes the player and stock variables. + * + * @param player the player that will play the stock game + * @param stocks the list of stocks to populate the exchange + */ + public SetupResult(Player player, List stocks) { + this.player = player; + this.stocks = stocks; + } + + /** + * Returns the player object containing the set name and starting money. + * + * @return the player object for the player playing the game + */ + public Player getPlayer() { + return player; + } + + /** + * A list of stocks that will be used for buying and selling on the exchange. + * + * @return a list of stock objects + */ + public List getStocks() { + return stocks; + } + } + + private final Stage stage; + + /** + * Initializes the initial stage for the view setting up the relevant game data. + * + * @param stage the initial stage to be used + */ + public GameSetupController(Stage stage) { + this.stage = stage; + } + + /** + * Sets up the initial data for the {@code Player} and {@code Exchange}. + * + *

Uses default values if the player does not enter their desired values: + *

    + *
  • Player name: Player
  • + *
  • Player starting money: 5000
  • + *
  • CSV file: resources/sp500.csv
  • + *

+ * + * @return a SetupResult object containing data for the player and the exchange + */ + public SetupResult setupNewGame() { + PlayerNameChooserView playerNameDialog = new PlayerNameChooserView(); + // Player name + String playerName = + playerNameDialog.getDialog().orElse("Player"); + + if (playerName.isBlank()) { + playerName = "Player"; + } + + // Starting money + PlayerStartingMoneyChooserView playerStartingMoneyDialog = new PlayerStartingMoneyChooserView(); + + BigDecimal startingMoney = + playerStartingMoneyDialog.getDialog() + .orElse(new BigDecimal("5000")); + + + // .csv File + CsvChooserView csvChooser = new CsvChooserView(); + File csvFile = csvChooser.getDialog(stage); + + List stocks; + + if (csvFile == null) { + stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv"); + } else { + stocks = FileHandler.readStocksFromFile(csvFile.getParent(), csvFile.getName()); + + if (stocks.isEmpty()) { + stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv"); + } + } + + Player player = new Player(playerName, startingMoney); + + return new SetupResult(player, stocks); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java new file mode 100644 index 0000000..5b743a9 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java @@ -0,0 +1,58 @@ +package no.ntnu.gruppe53.controller; + +import java.util.List; +import javafx.application.Platform; +import no.ntnu.gruppe53.service.LanguageManager; +import no.ntnu.gruppe53.service.LanguageOption; +import no.ntnu.gruppe53.service.LanguageRegistry; +import no.ntnu.gruppe53.view.StartView; + +/** + * The controller for {@link StartView}, + * the first view for the player upon launching the app. + * + *

Contains three choices: + *

    + *
  • New Game
  • + *
  • Language Select
  • + *
  • Quit
  • + *

+ * + *

Uses a {@link LanguageManager} to change the UI language.

+ */ +public class StartViewController { + /** + * Handles the events when "New Game" and "Quit" is pressed. + * + * @param view the StartView to be displayed + * @param vm the {@link ViewController} to handle switching of views + */ + public StartViewController(StartView view, ViewController vm) { + view.setOnNewGame(() -> { + GameController gameController = new GameController(vm); + gameController.startNewGame(); + }); + + view.setOnLanguage(() -> { + LanguageManager lm = LanguageManager.getInstance(); + List availableLanguages = LanguageRegistry.getLanguages(); + + int currentIndex = 0; + + for (int i = 0; i < availableLanguages.size(); i++) { + if (availableLanguages.get(i).locale().equals(lm.getLocale())) { + currentIndex = i; + break; + } + } + + int nextIndex = (currentIndex + 1) % availableLanguages.size(); + LanguageOption nextLanguage = availableLanguages.get(nextIndex); + + lm.setLocale(nextLanguage.locale()); + }); + + + view.setOnQuit(Platform::exit); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java new file mode 100644 index 0000000..f24cfeb --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java @@ -0,0 +1,82 @@ +package no.ntnu.gruppe53.controller; + +import java.util.HashMap; +import java.util.Map; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import no.ntnu.gruppe53.view.FooterBar; +import no.ntnu.gruppe53.view.NavigationBar; + +/** + * Controller for setting and switching between views. + * + *

Uses a {@link HashMap} as an archive for registered views that + * are possible to switch between.

+ */ +public class ViewController { + private final Stage stage; + private final Map views = new HashMap<>(); + private final BorderPane mainLayout; + + /** + * Initializes a ViewController using the {@link Stage} as the first default view. + * + *

Uses a {@link BorderPane} layout for views.

+ */ + public ViewController(Stage stage) { + this.stage = stage; + this.mainLayout = new BorderPane(); + Scene scene = new Scene(mainLayout, 1200, 900); + this.stage.setScene(scene); + } + + /** + * Returns the stage of the ViewController. + * + * @return the stage of the ViewController. + */ + public Stage getStage() { + return this.stage; + } + + /** + * Sets the top and bottom part of the BorderPane. + * + *

Uses {@link NavigationBar} as the top bar, + * and {@link FooterBar} as the bottom bar.

+ */ + public void setGlobalPanels(NavigationBar navigationBar, FooterBar footerBar) { + this.mainLayout.setTop(navigationBar); + this.mainLayout.setBottom(footerBar); + } + + /** + * Adds a view to the stored archive of possible views. + * + * @param name the name/key to refer to the view when switching view + * @param view the view to be added + */ + public void addView(String name, Parent view) { + views.put(name, view); + } + + /** + * Switches the to the given view based on the string key + * stored in the view archive. + * + *

Only switches the center pane.

+ * + * @param name the name/key of the view to switch to + */ + public void switchView(String name) { + Parent view = views.get(name); + + if (view == null) { + throw new IllegalArgumentException("No views found matching: " + name); + } + + mainLayout.setCenter(view); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java new file mode 100644 index 0000000..86fef14 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java @@ -0,0 +1,284 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collections; +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; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Represents an exchange/market containing {@link Stock}s and allows + * a {@link Player} to buy {@link Share}s of said stocks. + * + *

Contains method to advance the week of the market, finding stocks, + * gainers, and losers on the exchange.

+ */ +public class Exchange { + private final String name; + private int week; + private final Map stockMap; + private final Random random; + private final ObservableList stocks = FXCollections.observableArrayList(); + private final ObjectProperty currentWeekProperty = new SimpleObjectProperty<>(); + private final TransactionFactory purchaseFactory; + private final TransactionFactory saleFactory; + + /** + * Sets the name of the exchange and the list of stocks to populate it. + * + *

Validates that the values are proper before creating the exchange.

+ * + *

Initializes factories for creating a {@link Transaction}.

+ * + * @param name the name of the exchange + * @param stocks the list of stocks to be tradeable on the exchange + * @throws IllegalArgumentException if exchange is null or blank, if list of stocks is null, or if + * the list of stocks contains a null or duplicate symbol stock + */ + public Exchange(String name, List stocks) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Exchange name cannot be null or blank"); + } + + if (stocks == null) { + throw new IllegalArgumentException("Stock list cannot be null"); + } + + this.name = name; + this.week = 1; + this.currentWeekProperty.set(week); + this.stockMap = new HashMap<>(); + this.random = new Random(); + + for (Stock stock : stocks) { + if (stock == null) { + throw new IllegalArgumentException("Stock cannot be null"); + } + + if (stockMap.containsKey(stock.getSymbol())) { + throw new IllegalArgumentException("Duplicate stock symbol: " + stock.getSymbol()); + } + + stockMap.put(stock.getSymbol(), stock); + this.stocks.add(stock); + } + + this.purchaseFactory = new PurchaseFactory(); + this.saleFactory = new SaleFactory(); + } + + /** + * Returns the name of the exchange. + * + * @return the name of the exchange + */ + public String getName() { + return name; + } + + /** + * Returns the current week of the exchange. + * + * @return the current week of the exchange + */ + public int getWeek() { + return week; + } + + /** + * Checks whether a stock with the given symbol/ticker exists in the exchange. + * + *

Returns {@code true} if it does,v{@code false} otherwise.

+ * + * @param symbol the symbol/ticker of the stock + * @return {@code true} if the exchange contains the stock, otherwise {@code false} + */ + public boolean hasStock(String symbol) { + return stockMap.containsKey(symbol); + } + + /** + * Returns the stock object matching the given symbol/ticker. + * + * @param symbol the stock symbol/ticker + * @return the stock object matching the symbol/ticker + * @throws IllegalArgumentException if search symbol is null or symbol is not in stockMap + */ + public Stock getStock(String symbol) { + if (symbol == null || !stockMap.containsKey(symbol)) { + throw new IllegalArgumentException("Stock not found: " + symbol); + } + return stockMap.get(symbol); + } + + /** + * Returns an {@link ObservableList} of the list of stocks for JavaFX to observe. + * + * @return an observable list of the stocks in the exchange + */ + public ObservableList getObservableStocks() { + return stocks; + } + + /** + * Returns a list of stocks matching the search term in either the symbol/ticker or company name. + * + * @param searchTerm the string to search for + * @return a list of stocks matching the search term + */ + public List findStocks(String searchTerm) { + if (searchTerm == null || searchTerm.isBlank()) { + return Collections.emptyList(); + } + + String search = searchTerm.toLowerCase(); + return stockMap.values().stream() + .filter(stock -> stock.getSymbol().toLowerCase().contains(search) + || + stock.getCompany().toLowerCase().contains(search)) + .collect(Collectors.toList()); + } + + /** + * Represents a player buying a share on the exchange. + * + *

Creates a {@link Purchase} transaction for the + * purchase.

+ * + * @param symbol the symbol/ticker of the stock + * @param quantity the quantity of shares to buy for said stock + * @param player the player making the purchase + * @return a purchase transaction containing all details of the purchase + * @throws IllegalArgumentException if player or quantity is null + */ + public Transaction buy(String symbol, BigDecimal quantity, Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } + + if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + + Stock stock = getStock(symbol); + Share share = new Share(stock, quantity, stock.getSalesPrice()); + Transaction transaction = purchaseFactory.create(share, week); + transaction.commit(player); + + return transaction; + } + + /** + * Represents a player selling a share on the exchange. + * + *

Creates a {@link Sale} transaction for the sale.

+ * + * @param share the share to be sold + * @param player the player selling the share + * @return a sale transaction containing all details of the sale + * @throws IllegalArgumentException if player or share is null + */ + public Transaction sell(Share share, Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } + + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); + } + + Transaction transaction = saleFactory.create(share, week); + transaction.commit(player); + return transaction; + } + + /** + * Advances the timeline of the exchange by 1 week. + * + *

Uses a randomized value for the increase and decrease + * of a stocks sales price, simulating market fluctuations on a weekly basis.

+ */ + public void advance() { + this.week++; + this.currentWeekProperty.set(week); + + double negativePriceChange = -0.05; + double positivePriceChange = 0.1; + + for (Stock stock : stockMap.values()) { + BigDecimal currentPrice = stock.getSalesPrice(); + double randomPriceMultiplier = negativePriceChange + + (positivePriceChange * random.nextDouble()); + BigDecimal bdRandomPriceMultiplier = BigDecimal.valueOf(1 + randomPriceMultiplier); + + BigDecimal changedPrice = currentPrice.multiply(bdRandomPriceMultiplier) + .setScale(2, RoundingMode.HALF_UP); + + if (changedPrice.compareTo(BigDecimal.ZERO) <= 0) { + changedPrice = new BigDecimal(1); + } + + stock.addNewSalesPrice(changedPrice); + } + } + + /** + * Sorts the stocks in descending order and returns a list of stocks. + * + * @param entries amount of entries in the list to display + * @param comparator the comparator used to compare the stocks + * @return a list of stocks + * @throws IllegalArgumentException if the amount of entries is negative + */ + private List getTopStocks(int entries, Comparator comparator) { + if (entries < 0) { + throw new IllegalArgumentException("Amount of entries in list cannot be negative."); + } + + return stockMap.values().stream() + .sorted(comparator) + .limit(entries) + .collect(Collectors.toList()); + } + + /** + * Lists the given amount of stocks sorted by highest increase in sales price since last week. + * + * @param entries amount of stocks to be shown in list + * @return a descending list of stocks with the highest price increase since last week + * @throws IllegalArgumentException if number of entries in list is negative + */ + public List getGainers(int entries) { + return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange).reversed()); + } + + /** + * Lists the given amount of stocks sorted by the stocks with the lowest price increase (or + * highest price decrease). + * + * @param entries amount of stocks to be shown in the list + * @return a descending list of stocks with the highest price decrease or + * lowest price increase + * @throws IllegalArgumentException if number of entries in list is negative + */ + public List getLosers(int entries) { + return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange)); + } + + /** + * Returns an observable of the current week. + * + * @return an observable of the current week + */ + public ObjectProperty getCurrentWeekProperty() { + return currentWeekProperty; + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Player.java b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java new file mode 100644 index 0000000..6489d53 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java @@ -0,0 +1,204 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * Represents a participant in the trading game. + * + *

A {@code Player} has a name, a monetary balance, a {@link Portfolio} + * of owned {@link Share}s, and a {@link TransactionArchive} containing + * a history of the player's transactions.

+ * + *

The player starts with an initial amount of money and may gain or + * lose funds through transactions.

+ */ +public class Player { + private final String name; + private final BigDecimal startingMoney; + private BigDecimal money; + private final Portfolio portfolio; + private final TransactionArchive transactionArchive; + + private final ObjectProperty moneyProperty = new SimpleObjectProperty<>(); + private final ObjectProperty netWorthProperty = new SimpleObjectProperty<>(); + + /** + * Creates a player with a given starting balance. + * + *

Subscribes to the netWorth property to notify of changes in the player's net worth.

+ * + * @param name the player's name; must not be {@code null} or blank + * @param startingMoney the initial monetary balance of the + * player; must not be {@code null} or negative + * @throws IllegalArgumentException if {@code name} is {@code null} or + * blank, or if {@code startingMoney} + * is {@code null}, negative, or 0 + */ + public Player(String name, BigDecimal startingMoney) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Player name must not be null or empty."); + } + + if (startingMoney == null || startingMoney.signum() <= 0) { + throw new IllegalArgumentException("Starting money must not be null, negative, nor 0."); + } + + this.name = name; + this.startingMoney = startingMoney; + this.money = startingMoney; + this.moneyProperty.set(startingMoney); + this.portfolio = new Portfolio(); + this.transactionArchive = new TransactionArchive(); + + portfolio.netWorthProperty().subscribe(price -> updateNetWorth()); + updateNetWorth(); + } + + /** + * Returns an {@link ObjectProperty} of the player's net worth. + * + * @return an observable of the player's net worth + */ + public ObjectProperty getNetWorthProperty() { + return netWorthProperty; + } + + /** + * Mirrors the change in the player's net worth to the observable property. + */ + private void updateNetWorth() { + netWorthProperty.set(getNetWorth()); + } + + /** + * Returns the full name of the player. + * + * @return the player's name + */ + public String getName() { + return this.name; + } + + /** + * Returns the player's current monetary balance. + * + * @return the player's current balance. + */ + public BigDecimal getMoney() { + return this.money; + } + + /** + * Increases the players monetary balance by the given amount. + * + *

Money added must be a {@code positive} number.

+ * + * @param amount the amount to add; must be positive and not {@code null} + * @throws IllegalArgumentException if {@code amount} is {@code null} + * or not positive + */ + public void addMoney(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("Amount must not be null."); + } + + if (amount.signum() <= 0) { + throw new IllegalArgumentException("Amount must be positive."); + } + + this.money = this.money.add(amount); + this.moneyProperty.set(this.money); + updateNetWorth(); + } + + /** + * Decreases the player's monetary balance by the given amount. + * + *

The player's monetary balance must not decrease below {@code 0} + * (overdrawn not possible).

+ * + * @param amount the amount to subtract; must not be negative or {@code null} + * @throws IllegalArgumentException if the amount is not positive, + * {@code null}, or insufficient funds are available + */ + public void withdrawMoney(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("Amount must not be null."); + } + + if (amount.signum() <= 0) { + throw new IllegalArgumentException("Amount must be positive."); + } + + if (this.money.subtract(amount).signum() < 0) { + throw new IllegalArgumentException("Insufficient funds."); + } + + this.money = this.money.subtract(amount); + this.moneyProperty.set(this.money); + updateNetWorth(); + } + + /** + * Returns an observable of the player's current monetary funds. + * + * @return an observable of the player's current money + */ + public ObjectProperty getMoneyProperty() { + return moneyProperty; + } + + /** + * Return's the player's portfolio of shares. + * + * @return the player's portfolio + */ + public Portfolio getPortfolio() { + return this.portfolio; + } + + /** + * Returns the archive of the player's transaction history. + * + * @return the player's transaction archive + */ + public TransactionArchive getTransactionArchive() { + return this.transactionArchive; + } + + /** + * Returns the player's current net worth. + * + *

The net worth is the sum of the player's current money and + * all the value of their shares in their portfolio.

+ * + * @return the player's current net worth + */ + public BigDecimal getNetWorth() { + return money.add(portfolio.getNetWorth()); + } + + /** + * Returns the current status/title of the player, representing their achievements. + * + * @param exchange the exchange the player is trading at + * @return a string representing the players current status + */ + public String getStatus(Exchange exchange) { + String status = "Novice"; + BigDecimal ratio = getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN); + + if (ratio.compareTo(new BigDecimal("1.20")) >= 0 && exchange.getWeek() >= 10) { + status = "Investor"; + } + + if (ratio.compareTo(new BigDecimal("2")) >= 0 && exchange.getWeek() >= 20) { + status = "Speculator"; + } + + return status; + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java new file mode 100644 index 0000000..31fda6d --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java @@ -0,0 +1,162 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Represents the portfolio of the Player, containing information + * about which {@link Share}s are owned. + * + *

A {@code Portfolio} manages a mutable collection of shares, allowing + * shares to be added, removed, and queried.

+ * + *

Shares are associated with a unique Transaction, however, multiple + * shares from the same stock can exist in the portfolio.

+ */ +public class Portfolio { + private final ObservableList shares; + private final ObjectProperty netWorthProperty = + new SimpleObjectProperty<>(BigDecimal.ZERO); + + /** + * Creates an empty portfolio for the player. + */ + public Portfolio() { + this.shares = FXCollections.observableArrayList(); + } + + /** + * Adds a share to the player's portfolio. + * + *

The {@link Stock} must not be {@code null} and the share quantity + * and purchase price must be above zero.

+ * + * @param share the share owned by the player; must not be {@code null} + * @return {@code true} if the share was added successfully, + * {@code false} otherwise + * @throws IllegalArgumentException if {@code share} is {@code null} + */ + public boolean addShare(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); + } + + this.shares.add(share); + + share.getStock().salesPriceProperty() + .subscribe(price -> updateNetWorth()); + + updateNetWorth(); + + return true; + } + + /** + * Removes a share from the player's portfolio. + * + *

The list must already exist in the portfolio to be successfully + * removed.

+ * + * @param share the share being removed; must not be {@code null} + * @return {@code true} if the share removal was successful, + * {@code false} otherwise. + * @throws IllegalArgumentException if {@code share} is {@code null} + */ + public boolean removeShare(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); + } + + boolean removed = this.shares.removeIf(s -> s.equals(share)); + + if (removed) { + updateNetWorth(); + } + + return removed; + } + + /** + * Returns a list of the shares currently in the player's portfolio. + * + * @return a list of the shares in this portfolio + */ + public ObservableList getShares() { + return this.shares; + } + + /** + * Returns a list of the shares that belongs to the stock with the + * given ticker symbol. + * + * @param symbol the ticker symbol of the stock to search for; must not be + * {@code null} or blank + * @return a list of the player's shares in the stock with the given + * stock symbol; an empty list if none are found + * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank + */ + public List getShares(String symbol) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); + } + + var tempSymbols = new ArrayList(); + for (Share s : this.shares) { + + if (s.getStock().getSymbol().equals(symbol)) { + tempSymbols.add(s); + } + } + + return tempSymbols; + } + + /** + * Determines whether the player's portfolio contains the specified + * share. + * + * @param share the share to search for in this portfolio + * @return {@code true} if the player's portfolio contains the share + * {@code false} otherwise + */ + public boolean contains(Share share) { + return shares.stream().anyMatch(s -> s.equals(share)); + } + + /** + * Calculates the net total value of the shares in the portfolio. + * + *

Portfolio value is based on current market price, not transaction rules.

+ * + * @return the total market value of the portfolio + */ + public BigDecimal getNetWorth() { + return this.shares.stream() + .map(share -> + share.getStock().getSalesPrice() + .multiply(share.getQuantity()) + ) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * Returns an {@link ObjectProperty} of the net worth of the portfolio. + * + * @return an observable of the portfolio's net worth + */ + public ObjectProperty netWorthProperty() { + return netWorthProperty; + } + + /** + * Mirrors the portfolio's net worth to the observable. + */ + private void updateNetWorth() { + netWorthProperty.set(getNetWorth()); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java b/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java new file mode 100644 index 0000000..ddb3ada --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java @@ -0,0 +1,63 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; + +/** + * Represents a purchase {@link Transaction} where a {@link Player} purchases + * a {@link Share}. + * + *

A purchase can only be commited if: + *

    + *
  • The player has sufficient funds.
  • + *
  • The transaction has not already been committed.
  • + *
+ *

+ */ + +public class Purchase extends Transaction { + + /** + * Constructs a purchase transaction for the given share and week. + * + * @param share the share to be purchased; must not be {@code null} + * @param week the week when the purchase transaction occurs; must be 1 or greater + */ + + public Purchase(Share share, int week) { + super(share, week, new PurchaseCalculator(share)); + } + + /** + * Commits the purchase to the given player, making the + * transaction unique. + * + *

If the transaction is not already committed, and the player + * has sufficient funds, the transaction will: + *

    + *
  • Withdraw money from the player
  • + *
  • Add the share to the player's {@link Portfolio}
  • + *
  • Archive the transaction in the player's + * {@link TransactionArchive}
  • + *
+ *

+ * + * @param player the player performing the transaction; must not be {@code null} + * @throws IllegalArgumentException if {@code player} is {@code null} + */ + + @Override + public void commit(Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null."); + } + + if (!this.isCommitted() && ((player.getMoney().subtract(this.getCalculator().calculateTotal())) + .compareTo(BigDecimal.ZERO) >= 0)) { + + player.withdrawMoney(this.getCalculator().calculateTotal()); + player.getPortfolio().addShare(this.getShare()); + player.getTransactionArchive().add(this); + super.commit(player); + } + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java new file mode 100644 index 0000000..08865b9 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java @@ -0,0 +1,110 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * Calculates the costs associated with a {@link Purchase} of a given + * amount of {@link Share}s on the {@link Exchange}. + */ +public class PurchaseCalculator implements TransactionCalculator { + private final BigDecimal purchasePrice; + private final BigDecimal quantity; + + // Objectproperties + private final ObjectProperty grossProperty = new SimpleObjectProperty<>(); + private final ObjectProperty commissionProperty = new SimpleObjectProperty<>(); + private final ObjectProperty totalProperty = new SimpleObjectProperty<>(); + + /** + * Sets the purchase price and quantity based on data from the given share. + * + *

Also sets up the ObjectProperties of the different calculations such + * that Subscriptions can listen to the calculations

+ * + * @param share the share to calculate purchase costs for + */ + public PurchaseCalculator(Share share) { + this.purchasePrice = share.getPurchasePrice(); + this.quantity = share.getQuantity(); + + this.grossProperty.set(calculateGross()); + this.commissionProperty.set(calculateCommission()); + this.totalProperty.set(calculateTotal()); + + } + + /** + * Calculates the gross purchase price of the given share. + * + * @return the gross associated cost of purchasing the share + */ + @Override + public BigDecimal calculateGross() { + return this.quantity.multiply(this.purchasePrice); + } + + /** + * Calculates the commission of the exchange for purchasing a share. + * + *

Commission is set to a fixed 0.5% of gross purchase price.

+ * + * @return the commission taken by the exchange + */ + @Override + public BigDecimal calculateCommission() { + BigDecimal commission = new BigDecimal("0.005"); + + return calculateGross().multiply(commission); + } + + /** + * Calculates the tax for a purchase. + * + *

For a purchase, there is no tax.

+ * + * @return the tax for a purchase, in this case zero + */ + @Override + public BigDecimal calculateTax() { + return new BigDecimal(0); + } + + /** + * Calculates the total costs associated with the purchase of a share. + * + * @return the net total cost of purchasing a share + */ + @Override + public BigDecimal calculateTotal() { + return calculateGross().add(calculateCommission()).add(calculateTax()); + } + + /** + * Returns an observable of the calculated gross. + * + * @return an observable of the calculated gross + */ + public ObjectProperty getGrossProperty() { + return grossProperty; + } + + /** + * Returns an observable of the calculated commission. + * + * @return an observable of the calculated commission + */ + public ObjectProperty getCommissionProperty() { + return commissionProperty; + } + + /** + * Returns an observable of the calculated total. + * + * @return an observable of the calculated total + */ + public ObjectProperty getTotalProperty() { + return totalProperty; + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java new file mode 100644 index 0000000..a05169f --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java @@ -0,0 +1,19 @@ +package no.ntnu.gruppe53.model; + +/** + * Creates a {@link Purchase} {@link Transaction} of a {@link Share}. + */ +public class PurchaseFactory extends TransactionFactory { + + /** + * Returns the purchase transaction for the given share and week. + * + * @param share the share to be purchased + * @param week the week of purchase + * @return the purchase transaction + */ + @Override + protected Transaction createTransaction(Share share, int week) { + return new Purchase(share, week); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java new file mode 100644 index 0000000..ff418ee --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java @@ -0,0 +1,65 @@ +package no.ntnu.gruppe53.model; + +/** + * Represents a sale {@link Transaction} where a {@link Player} sells + * a {@link Share}. + * + *

A sale can only be commited if: + *

    + *
  • The share is in the players {@link Portfolio}
  • + *
  • The transaction has not already committed
  • + *
+ *

+ */ + +public class Sale extends Transaction { + + /** + * Constructs a sale transaction for the given share and week. + * + * @param share the share to be sold; must not be {@code null} + * @param week the week when the sale transaction occurs; must be greater than 0 + */ + + public Sale(Share share, int week) { + super(share, week, new SaleCalculator(share)); + } + + /** + * Commits the sale to the given player, making the + * transaction unique. + * + *

If the player ows the share, the transaction will: + *

    + *
  • Add money to the player
  • + *
  • Remove the share from the player's portfolio
  • + *
  • Archive the transaction in the player's {@link TransactionArchive}
  • + *
+ *

+ * + * @param player the player performing the transaction; must not be {@code null} + * @throws IllegalArgumentException if {@code player} is {@code null} + * @throws IllegalStateException if trying to sell a share not in the player's portfolio or + * if transactions has already been committed. + */ + + @Override + public void commit(Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } + + if (this.isCommitted()) { + throw new IllegalStateException("Transaction has already been committed."); + } + + if (!player.getPortfolio().contains(this.getShare())) { + throw new IllegalStateException("You do not own this share in your portfolio."); + } + + player.addMoney(this.getCalculator().calculateTotal()); + player.getPortfolio().removeShare(this.getShare()); + player.getTransactionArchive().add(this); + super.commit(player); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java new file mode 100644 index 0000000..a108d86 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java @@ -0,0 +1,153 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * Calculates the costs associated with the {@link Sale} of a {@link Share} on the {@link Exchange}, + * and the net total gain of selling said share. + */ +public class SaleCalculator implements TransactionCalculator { + private final BigDecimal purchasePrice; + private final BigDecimal salesPrice; + private final BigDecimal quantity; + + // Objectproperties + private final ObjectProperty grossProperty = new SimpleObjectProperty<>(); + private final ObjectProperty commissionProperty = new SimpleObjectProperty<>(); + private final ObjectProperty taxProperty = new SimpleObjectProperty<>(); + private final ObjectProperty totalProperty = new SimpleObjectProperty<>(); + private final ObjectProperty profitProperty = new SimpleObjectProperty<>(); + + /** + * Sets the quantity, purchase and sales price based on the given share. + * + * @param share the share to calculate the costs and net total of a sale + */ + public SaleCalculator(Share share) { + this.purchasePrice = share.getPurchasePrice(); + this.quantity = share.getQuantity(); + this.salesPrice = share.getStock().getSalesPrice(); + + this.grossProperty.set(calculateGross()); + this.commissionProperty.set(calculateCommission()); + this.taxProperty.set(calculateTax()); + this.totalProperty.set(calculateTotal()); + this.profitProperty.set(calculateTotal().subtract(purchasePrice.multiply(quantity))); + } + + /** + * Calculates the gross sales price of the given share. + * + * @return the gross associated gain from selling the share + */ + @Override + public BigDecimal calculateGross() { + return quantity.multiply(salesPrice) + .setScale(2, RoundingMode.HALF_UP); + } + + /** + * Calculates the exchange's commission for selling the share. + * + *

The commission is set to a fixed 1% of the gross total sale value.

+ * + * @return the commission taken by the exchange + */ + @Override + public BigDecimal calculateCommission() { + + BigDecimal commissionRate = new BigDecimal("0.01"); + + return calculateGross() + .multiply(commissionRate) + .setScale(2, RoundingMode.HALF_UP); + } + + /** + * Calculates the taxable profit from the sale. + * + *

Only calculates tax for profitable sales.

> + * + * @return the tax amount + */ + @Override + public BigDecimal calculateTax() { + + BigDecimal taxRate = new BigDecimal("0.30"); + + BigDecimal originalCost = purchasePrice.multiply(quantity); + + BigDecimal profit = calculateGross() + .subtract(calculateCommission()) + .subtract(originalCost); + + if (profit.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); + } + + return profit.multiply(taxRate) + .setScale(2, RoundingMode.HALF_UP); + } + + /** + * Returns the net total gain for selling the share. + * + * @return the net total winnings of selling the share + */ + @Override + public BigDecimal calculateTotal() { + + return calculateGross() + .subtract(calculateCommission()) + .subtract(calculateTax()) + .setScale(2, RoundingMode.HALF_UP); + } + + /** + * Returns an observable of the calculated gross. + * + * @return an observable of the calculated gross + */ + public ObjectProperty getGrossProperty() { + return grossProperty; + } + + /** + * Returns an observable of the calculated commission. + * + * @return an observable of the calculated commission + */ + public ObjectProperty getCommissionProperty() { + return commissionProperty; + } + + /** + * Returns an observable of the calculated tax. + * + * @return an observable of the calculated tax + */ + public ObjectProperty getTaxProperty() { + return taxProperty; + } + + /** + * Returns an observable of the calculated total. + * + * @return an observable of the calculated total + */ + public ObjectProperty getTotalProperty() { + return totalProperty; + } + + /** + * Returns an observable of the calculated profit. + * + * @return an observable of the calculated profit + */ + public ObjectProperty getProfitProperty() { + return profitProperty; + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java new file mode 100644 index 0000000..9128a74 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java @@ -0,0 +1,19 @@ +package no.ntnu.gruppe53.model; + +/** + * Creates a {@link Sale} {@link Transaction} of a {@link Share}. + */ +public class SaleFactory extends TransactionFactory { + + /** + * Returns the sale transaction of the given share and week. + * + * @param share the share to be sold + * @param week the week of the sale + * @return the sale transaction + */ + @Override + protected Transaction createTransaction(Share share, int week) { + return new Sale(share, week); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Share.java b/millions/src/main/java/no/ntnu/gruppe53/model/Share.java new file mode 100644 index 0000000..229c473 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Share.java @@ -0,0 +1,76 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; + +/** + * Represents a holding of a specific {@link Stock}. + * + *

A {@code Share} contains the quantity of shares owned and + * the price per share at the time of purchase. Future price + * fluctuations are not included in share. + *

+ */ + +public class Share { + private final Stock stock; + private final BigDecimal quantity; + private final BigDecimal purchasePrice; + + /** + * Creates a share for a given stock. + * + * @param stock the stock associated with the share; must not be {@code null} + * @param quantity the number of shares purchased; must not be {@code null}, negative, or zero + * @param purchasePrice price per share at the time of purchase; + * must not be {@code null}, negative or zero + * @throws IllegalArgumentException if {@code stock} is {@code null}, + * or {@code quantity} is {@code null}, + * negative, or zero, or {@code purchasePrice} + * is {@code null}, negative, or zero + */ + public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { + if (stock == null) { + throw new IllegalArgumentException("Stock cannot be null."); + } + + if (quantity == null || quantity.signum() <= 0) { + throw new IllegalArgumentException("Quantity cannot be null, negative, or zero."); + } + + if (purchasePrice == null || purchasePrice.signum() <= 0) { + throw new IllegalArgumentException("PurchasePrice cannot be null, negative, or zero."); + } + + this.stock = stock; + this.quantity = quantity; + this.purchasePrice = purchasePrice; + } + + /** + * Returns the stock the share is associated with. + * + * @return the stock + */ + public Stock getStock() { + return stock; + } + + /** + * Returns the quantity of shares owned in the stock. + * + * @return the quantity of shares + */ + + public BigDecimal getQuantity() { + return quantity; + } + + /** + * Returns the purchase price per share for the stock at time of purchase. + * + * @return the purchase price of the shares. + */ + public BigDecimal getPurchasePrice() { + return purchasePrice; + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java new file mode 100644 index 0000000..04c53c8 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java @@ -0,0 +1,163 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * Represents a stock with its ticker symbol, company name, + * and a list of sales prices. + * + *

The list is a historical representation of price changes + * over time for the given stock. + *

+ * + *

The most recently added price represents the current market price. + *

+ * + *

A {@code Stock} instance always contains at least one price, + * instantiated at construction.

+ */ +public class Stock { + private final String symbol; + private final String company; + private final List prices; + + private final ObjectProperty salesPrice; + + /** + * Creates a stock with an already defined sales price. + * + * @param symbol the stock's unique ticker symbol (e.g. "AAPL"); must not be {@code null} or blank + * @param company the full name of the company; must not be {@code null} or blank + * @param salesPrice the current price per share; must not be {@code null}, negative, or zero + * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank, {@code company} is + * {@code null} or blank, or {@code salesPrice} is {@code null}, + * negative, or zero + */ + public Stock( + String symbol, String company, BigDecimal salesPrice) { + + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or empty."); + } + + if (company == null || company.isBlank()) { + throw new IllegalArgumentException("Company cannot be null or empty."); + } + + if (salesPrice == null || salesPrice.signum() <= 0) { + throw new IllegalArgumentException("SalesPrice cannot be null, negative, or zero."); + } + + this.symbol = symbol; + this.company = company; + this.prices = new ArrayList<>(); + this.prices.add(salesPrice); + + this.salesPrice = new SimpleObjectProperty<>(salesPrice); + } + + /** + * Returns the stock's unique ticker symbol. + * + * @return the stock's ticker symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * Returns the full name of the company associated with the stock. + * + * @return the full name of the stock's company + */ + public String getCompany() { + return company; + } + + /** + * Returns the current sale price per share of the stock. + * + *

The current price is defined as the most recent price + * in the price history.

+ * + * @return the current sale price of the stock + */ + public BigDecimal getSalesPrice() { + return salesPrice.get(); + } + + /** + * Adds a new sales price to the stock's price history. + * + *

The added price becomes the new current market price.

+ * + *

The new price must be positive.

+ * + * @param price the new market price per share of the stock; + * must not be {@code null}, negative or zero + * @throws IllegalArgumentException if {@code price} is {@code null}, negative, or zero + */ + public void addNewSalesPrice(BigDecimal price) { + if (price == null || price.signum() <= 0) { + throw new IllegalArgumentException("Price cannot be null, negative, or zero."); + } + + this.prices.add(price); + this.salesPrice.set(price); + } + + /** + * Returns an {@link ObjectProperty} of the sales price. + * + * @return an observable of the sales price + */ + public ObjectProperty salesPriceProperty() { + return salesPrice; + } + + /** + * Returns a list of all the sales prices for this stock since (and including) week 1. + * + * @return a list of all the stock's sales prices + */ + public List getHistoricalPrices() { + return prices; + } + + /** + * Returns the maximum historical sales price since (and including) week 1 for the stock. + * + * @return the stock's highest sales price + */ + public BigDecimal getHighestPrice() { + return Collections.max(prices); + } + + /** + * Returns the minimum historical sales price since (and including) week 1 for the stock. + * + * @return the stock's lowest sales price + */ + public BigDecimal getLowestPrice() { + return Collections.min(prices); + } + + /** + * Returns the sales price increase/decrease from previous week to this week. Returns 0 if + * the price list only has 1 value. + * + * @return the net difference in sales price between the last and second last week + */ + public BigDecimal getLatestPriceChange() { + if (prices.size() >= 2) { + return prices.getLast().subtract(prices.get(prices.size() - 2)); + } else { + return BigDecimal.ZERO; + } + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java b/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java new file mode 100644 index 0000000..ee2f11c --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java @@ -0,0 +1,94 @@ +package no.ntnu.gruppe53.model; + +/** + * Represents a financial transaction of a {@link Share}. + * + *

A transaction occurs in a specific week and uses a + * {@link TransactionCalculator} to determine the monetary change. + * Transactions are unique and is committed to a {@link Player}'s + * {@link Portfolio}.

+ * + *

Subclasses define the nature of the transaction (e.g. purchase + * or sale) by implementing the {@link #commit(Player)}

+ */ +public abstract class Transaction { + private final Share share; + private final int week; + private final TransactionCalculator calculator; + protected boolean committed; + + /** + * Constructor for a transaction. + * + * @param share the share for the transaction; must not be {@code null} + * @param week the week the transaction takes place; must be greater than 0 + * @param calculator the transaction calculator used to + * compute the {@link Transaction} + * @throws IllegalArgumentException if {@code share} is {@code null} or {@code week} + * is less than 1 + */ + + protected Transaction(Share share, int week, TransactionCalculator calculator) { + if (week <= 0) { + throw new IllegalArgumentException("Week cannot be negative or zero"); + } + + this.share = share; + this.week = week; + this.calculator = calculator; + this.committed = false; + } + + /** + * Returns the share for the current transaction. + * + * @return the share belonging to the transaction + */ + public Share getShare() { + return this.share; + } + + /** + * Returns the week the transaction took place. + * + * @return the week for the transaction + */ + public int getWeek() { + return this.week; + } + + /** + * Returns the transaction calculator used to calculate + * the transaction. + * + * @return the transaction calculator used for the transaction + */ + + public TransactionCalculator getCalculator() { + return this.calculator; + } + + /** + * Returns whether the transaction has been committed to a player. + * + * @return {@code true} if committed, {@code false} otherwise + */ + + public boolean isCommitted() { + return this.committed; + } + + /** + * Commits the transaction to a player, making it unique. + * + *

Subclasses should define the business logic required before + * committing. Then {@code super.commit(player)} should be called + * once the transaction has been successfully applied.

+ * + * @param player the player performing the transaction; must not be {@code null} + */ + + public void commit(Player player) { + this.committed = true; + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java new file mode 100644 index 0000000..88e5f7b --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java @@ -0,0 +1,145 @@ +package no.ntnu.gruppe53.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * Maintains a history of all completed {@link Transaction}s. + * + *

A {@code TransactionArchive} stores transactions in the order + * they are added and provides methods to retrieve transactions + * filtered by type and week.

+ */ + +public class TransactionArchive { + private final ObservableList transactions; + + /** + * Creates an empty transaction archive. + */ + + public TransactionArchive() { + this.transactions = FXCollections.observableArrayList(); + } + + /** + * Adds a transaction to the archive. + * + * @param transaction the transaction being added; must not be {@code null} + * @return {@code true} if the transaction was successfully added + * {@code false} otherwise + * @throws IllegalArgumentException if {@code transaction} is {@code null} + */ + + public boolean add(Transaction transaction) { + if (transaction == null) { + throw new IllegalArgumentException("Transaction cannot be null."); + } + + this.transactions.add(transaction); + + return this.getTransactions().get(transactions.size() - 1).equals(transaction); + } + + /** + * Checks whether there are no recorded transactions in the archive. + * + * @return {@code true} if the archive is empty. + * {@code false} otherwise + */ + + public boolean isEmpty() { + return this.transactions.isEmpty(); + } + + /** + * Returns all the transactions contained in the archive. + * + * @return the list of transactions + */ + + public ObservableList getObservableTransactions() { + return this.transactions; + } + + /** + * Returns the transactions in the transaction archive. + * + * @return the transactions in the transaction archive + */ + public List getTransactions() { + return this.transactions; + } + + /** + * Returns all {@link Purchase} transactions for the given week. + * + * @param week the week number for transactions; must be greater than 0 + * @return a list of purchase transactions for the given week; + * empty if none exist + * @throws IllegalArgumentException if {@code week} is not greater than 0 + */ + + public List getPurchases(int week) { + if (week <= 0) { + throw new IllegalArgumentException("Week must be greater than 0."); + } + var purchaseList = new ArrayList(); + + for (Transaction t : this.transactions) { + if (t instanceof Purchase && t.getWeek() == week) { + purchaseList.add((Purchase) t); + } + } + return purchaseList; + } + + /** + * Returns all {@link Sale} transactions for the given week. + * + * @param week the week number for transactions; must be greater than 0 + * @return a list of sale transactions for the given week; + * empty if none exist + * @throws IllegalArgumentException if {@code week} is not greater than 0 + */ + + public List getSales(int week) { + if (week <= 0) { + throw new IllegalArgumentException("Week must be greater than 0."); + } + + var saleList = new ArrayList(); + + for (Transaction t : this.transactions) { + if (t instanceof Sale && t.getWeek() == week) { + saleList.add((Sale) t); + } + } + return saleList; + } + + /** + * Counts the number of distinct week numbers represented in the + * transaction archive. + * + *

A week is considered distinct if at least one transaction has + * occurred within that week. Multiple transactions in the span of the + * same week will only be counted as one distinct week.

+ * + * @return the number of unique week values in the archive; + * {@code 0} if the archive contains no transactions + */ + + public int countDistinctWeeks() { + var distinctWeeks = new HashSet(); + + for (Transaction t : this.transactions) { + distinctWeeks.add(t.getWeek()); + } + + return distinctWeeks.size(); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java new file mode 100644 index 0000000..c9f2097 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java @@ -0,0 +1,39 @@ +package no.ntnu.gruppe53.model; + +import java.math.BigDecimal; + +/** + * Represents the necessary calculations to perform a transaction. + * + *

{@link Purchase} and {@link Sale} are examples of implementations of a transaction.

+ */ +public interface TransactionCalculator { + /** + * Calculates the gross value of a transaction. + * + * @return the gross value of a transaction + */ + BigDecimal calculateGross(); + + /** + * Calculates the commission for the exchange of a transaction. + * + * @return the commission of a transaction + */ + BigDecimal calculateCommission(); + + /** + * Calculates the tax of a transaction. + * + * @return the tax of a transaction + */ + BigDecimal calculateTax(); + + /** + * Returns the total value of a transaction. + * + * @return the total value of a transaction + */ + BigDecimal calculateTotal(); + +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java new file mode 100644 index 0000000..cb27b9f --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java @@ -0,0 +1,40 @@ +package no.ntnu.gruppe53.model; + +/** + * Factory for creating a {@link Transaction} for a {@link Share} in an {@link Exchange}. + * + *

Subclasses implement the specific type of transaction to be used.

+ */ +public abstract class TransactionFactory { + + /** + * Creates a transaction. + * + * @param share the share involved + * @param week the transaction week + * @return a transaction + */ + public Transaction create(Share share, int week) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null."); + } + + if (week <= 0) { + throw new IllegalArgumentException("Week must be positive whole number."); + } + + return createTransaction(share, week); + } + + /** + * Creates a transaction from a share and week. + * + * @param share the share for the transaction + * @param week the week of the transaction + * @return a transaction for the share and week + */ + protected abstract Transaction createTransaction( + Share share, int week + ); + +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java new file mode 100644 index 0000000..a587c09 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java @@ -0,0 +1,155 @@ +package no.ntnu.gruppe53.service; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import no.ntnu.gruppe53.model.Stock; + +/** + * Handles reading and writing of .csv files for stocks. + */ +public class FileHandler { + /** + * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding. + * + * @param stockList the list of stocks to be written + * @param path directory path for the file + * @param filename name of the file (without or with .csv extension) + * @throws IllegalArgumentException if stockList, path, + * or filename is null, or if stockList is empty + */ + public static void writeStocksToFile(List stockList, String path, String filename) { + if (stockList == null) { + throw new IllegalArgumentException("StockList cannot be null"); + } + + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be null or blank"); + } + + if (filename == null || filename.isBlank()) { + throw new IllegalArgumentException( + "Filename cannot be null or blank"); + } + + path = path.trim(); + filename = filename.trim(); + String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv"; + + Path filePath = Paths.get(path, filenameWithExtension); + + Path dirPath = filePath.getParent(); + + if (!Files.exists(dirPath)) { + try { + Files.createDirectories(dirPath); + } catch (IOException e) { + System.out.println("ERROR: Could not create directories."); + e.printStackTrace(); + return; + } + } + + try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) { + writer.write("#Ticker,Name,Price"); + writer.newLine(); + + for (Stock stock : stockList) { + if (stock == null) { + continue; + } + + String line = String.join(",", + stock.getSymbol(), + stock.getCompany(), + stock.getSalesPrice().toString()); + writer.write(line); + writer.newLine(); + } + + } catch (IOException e) { + System.out.println("ERROR: Something went wrong while writing the CSV file."); + e.printStackTrace(); + } + } + + /** + * Reads a CSV file and converts it to a list of {@link Stock} objects. + * + * @param path directory path of the CSV file + * @param filename CSV file name (with or without .csv extension) + * @return list of Stock objects + * @throws IllegalArgumentException if path or filename is null or blank + */ + public static List readStocksFromFile(String path, String filename) { + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be null or blank."); + } + + if (filename == null || filename.isBlank()) { + throw new IllegalArgumentException("Filename cannot be null or blank."); + } + + path = path.trim(); + filename = filename.trim(); + String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv"; + + Path filePath = Paths.get(path, filenameWithExtension); + + List stocks = new ArrayList<>(); + if (!Files.exists(filePath)) { + System.out.println("ERROR: File not found: " + filePath.toAbsolutePath()); + return stocks; + } + + try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { + String line; + + while ((line = reader.readLine()) != null) { + line = line.trim(); + + if (line.isBlank() || line.startsWith("#")) { + continue; + } + + String[] values = line.split(",", -1); + + if (values.length != 3) { + System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line); + continue; + } + + String symbol = values[0].trim(); + String name = values[1].trim(); + BigDecimal price; + + try { + price = new BigDecimal(values[2].trim()); + } catch (NumberFormatException e) { + System.out.println("ERROR: Price is not a valid number: " + values[2]); + continue; + } + + if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) { + System.out.println("ERROR: Invalid stock data, skipping: " + line); + continue; + } + + stocks.add(new Stock(symbol, name, price)); + } + + } catch (IOException e) { + System.out.println("ERROR: Something went wrong while reading the CSV file."); + e.printStackTrace(); + } + + return stocks; + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java b/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java new file mode 100644 index 0000000..422b321 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java @@ -0,0 +1,21 @@ +package no.ntnu.gruppe53.service; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Service/helper class that formats a {@link BigDecimal} to a string with 2 decimal accuracy. + */ +public class FormatBigDecimal { + /** + * Static method used to format a Big Decimal to a string representation with 2 decimals accuracy. + * + *

Returns "0.00" if the BigDecimal is null.

+ * + * @param number the BigDecimal to be formated + * @return the string representation of the BigDecimal with 2 decimals accuracy. + */ + public static String formatNumber(BigDecimal number) { + return number == null ? "0.00" : number.setScale(2, RoundingMode.HALF_UP).toPlainString(); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java new file mode 100644 index 0000000..6d6356d --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java @@ -0,0 +1,101 @@ +package no.ntnu.gruppe53.service; + +import java.util.Locale; +import java.util.ResourceBundle; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +/** + * Manages the language used in the user-interface. + * + *

The possible languages to be used are set by {@link LanguageRegistry}.

+ */ +public class LanguageManager { + + /** + * Sets a default language and creates an observable {@link ResourceBundle}. + */ + private final ObjectProperty bundleProperty = + new SimpleObjectProperty<>(); + + /** + * Sets the language to the default locale at first instantiation. + */ + private LanguageManager() { + setLocale(LanguageRegistry.getDefaultLocale()); + } + + /** + * Static Inner Class to lazily create the instance. + */ + private static class LanguageManagerInner { + private static final LanguageManager INSTANCE = + new LanguageManager(); + } + + /** + * Returns the instance of the language manager. + * + * @return the instance of the language manager + */ + public static LanguageManager getInstance() { + return LanguageManagerInner.INSTANCE; + } + + /** + * Returns the observable resource bundle. + * + * @return an observable resource bundle + */ + public ObjectProperty getBundleProperty() { + return bundleProperty; + } + + /** + * Sets the locale of the instance to a given locale which determines the properties file to load + * values from. + * + * @param locale the locale to change to + */ + public void setLocale(Locale locale) { + ResourceBundle bundle = + ResourceBundle.getBundle("i18n.lang", locale); + + bundleProperty.set(bundle); + } + + /** + * Returns the string value of the given key in a .properties file. + * + * @param key the key to look for in the .properties file + * @return the value of the key + */ + public String getString(String key) { + return bundleProperty.get().getString(key); + } + + /** + * Returns the current locale. + * + * @return the current locale + */ + public Locale getLocale() { + return bundleProperty.get().getLocale(); + } + + /** + * Creates a {@link StringBinding} to a given string and associates it + * with a given key in a .properties file. + * + * @param key the key to associate the string binding with + * @return a string binding observable + */ + public StringBinding bindString(String key) { + return Bindings.createStringBinding( + () -> getString(key), + getBundleProperty() + ); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java new file mode 100644 index 0000000..cd607b1 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java @@ -0,0 +1,12 @@ +package no.ntnu.gruppe53.service; + +import java.util.Locale; + +/** + * Acts as a data-carrier representing a language with a string name and a {@link Locale}. + * + * @param name the name of the language + * @param locale the locale of the language + */ +public record LanguageOption(String name, Locale locale) {} + diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java new file mode 100644 index 0000000..490ab9f --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java @@ -0,0 +1,56 @@ +package no.ntnu.gruppe53.service; + +import java.util.List; +import java.util.Locale; + +/** + * Represents a registry of allowed {@link LanguageOption}s. + */ +public class LanguageRegistry { + + /** + * Sets all possible languages that can be used. + */ + private static final List languages = List.of( + new LanguageOption("English", new Locale("en")), + new LanguageOption("Norsk", new Locale("no")) + ); + + /** + * Sets the default language to be used. + * + *

Searches through the registry to find english and sets it as default.

+ */ + private static final LanguageOption defaultLang = + languages.stream() + .filter(lang -> lang.locale().getLanguage().equals("en")) + .findFirst() + .orElse(languages.getFirst()); + + /** + * Returns all registered languages. + * + * @return a list of registered languages + */ + public static List getLanguages() { + return languages; + } + + /** + * Returns the default language. + * + * @return the default language + */ + public static LanguageOption getDefaultLanguage() { + return defaultLang; + } + + /** + * Returns the default locale of the default language. + * + * @return the default locale + */ + public static Locale getDefaultLocale() { + return defaultLang.locale(); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java new file mode 100644 index 0000000..5e785cd --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java @@ -0,0 +1,79 @@ +package no.ntnu.gruppe53.service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import javafx.scene.chart.XYChart; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Stock; +import no.ntnu.gruppe53.view.StockLineChartView; + +/** + * Builds the {@link XYChart.Series} values for the {@link StockLineChartView}. + */ + +public class StockLineChartService { + + /** + * Builds the {@code XYChart.Series} for the {@code LineChart} of a single {@link Stock}. + * + *

Returns the {@code XYChart.Series}.

+ * + * @param stock the stock whose price values will be shown in the LineChart + * @param exchange the exchange containing the stock whose + * week number will be shown in the LineChart + * @return the {@code XYChart.Series} with + * the y-values from the price history of the stock, + * and the x-values from the week number of the exchange + * @throws IllegalArgumentException if there are more x-values than y-values or vice versa + */ + + public static XYChart.Series buildSeries(Stock stock, Exchange exchange) { + XYChart.Series series = new XYChart.Series<>(); + series.setName(stock.getSymbol()); + + // Check for 1-1 ratio of price and week list size for plotting + if (stock.getHistoricalPrices().size() != exchange.getWeek()) { + throw new IllegalArgumentException("Invalid Data: mismatch between prices and weeks. " + + "Should have 1-1 ratio."); + } + + int week = 1; + + for (BigDecimal price : stock.getHistoricalPrices()) { + series.getData().add( + new XYChart.Data<>(week, price.doubleValue())); + week++; + } + + return series; + } + + /** + * Builds the {@code XYChart.Series} for the {@code LineChart} of a list of {@code Stock}s. + * + *

Returns a list of the {@code XYChart.Series} for each stock.

+ * + * @param stocks the list of stocks whose price values will be shown + * in the LineChart + * @param exchange the exchange containing the stock whose week number + * will be shown in the LineChart + * @return the list of {@code XYChart.Series} with + * the y-values from the price history of the stock, + * and the x-values from the week number of the exchange + */ + + public static List> buildSeries( + List stocks, Exchange exchange) { + List> seriesList = new ArrayList<>(); + + for (Stock stock : stocks) { + XYChart.Series series = + buildSeries(stock, exchange); + + seriesList.add(series); + } + + return seriesList; + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java new file mode 100644 index 0000000..4264887 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java @@ -0,0 +1,51 @@ +package no.ntnu.gruppe53.view; + +import java.io.File; +import javafx.scene.control.Alert; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import no.ntnu.gruppe53.service.LanguageManager; + +/** + * Represents an {@link Alert} and {@link FileChooser} for + * informing user of .csv requirement and picking said file. + * + *

Uses a {@link LanguageManager} to insert set text to target language.

+ */ +public class CsvChooserView { + private final FileChooser fileChooser = new FileChooser(); + Alert newGameAlert = new Alert(Alert.AlertType.INFORMATION); + private final LanguageManager lm = LanguageManager.getInstance(); + + /** + * Returns the dialog for the alert and file chooser and exposes it to controllers. + * + * @param stage the stage for the dialog to be shown + * @return the dialog for the alert and file chooser + */ + public File getDialog(Stage stage) { + // Notifies the user about .csv selection + newGameAlert.setTitle(null); + newGameAlert.headerTextProperty().bind(lm.bindString("csvHeader")); + newGameAlert.contentTextProperty().bind(lm.bindString("csvContent")); + + // Sets the dimensions of the alert to show all text, and as a band-aid, be resizable + newGameAlert.setResizable(true); + + newGameAlert.getDialogPane().setPrefWidth(450); + newGameAlert.getDialogPane().setPrefHeight(350); + + newGameAlert.showAndWait(); + + // File chooser to choose .csv file + fileChooser.setTitle(lm.getString("csvFileChooserTitle")); + + // Limits to .csv file only + fileChooser.getExtensionFilters().clear(); + fileChooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter(lm.getString("csvFileFilterLabel"), "*.csv") + ); + + return fileChooser.showOpenDialog(stage); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java b/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java new file mode 100644 index 0000000..611e997 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java @@ -0,0 +1,213 @@ +package no.ntnu.gruppe53.view; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; +import no.ntnu.gruppe53.service.LanguageManager; + +/** + * Represents the End Game screen, showing the Level the Player reached, + * money earned in total after selling portfolio and gives the option + * to quit the program or start a new game. + */ +public class EndView extends BorderPane { + private Button newGameButton; + private Button quitButton; + + private Label statusLabel; + private Label moneyLabel; + + private final LanguageManager lm = LanguageManager.getInstance(); + + /** + * Sets the centerPane to the constructed centerPane. + */ + public EndView() { + this.setCenter(centerPane()); + } + + /** + * Constructs the centerPane using a {@link BorderPane}. + * + * @return a constructed borderpane + */ + private BorderPane centerPane() { + BorderPane pane = new BorderPane(); + pane.setPadding(new Insets(15, 12, 15, 12)); + // pane.setSpacing(10); + // Blue Background color + pane.setStyle("-fx-background-color: #00bcf0;"); + + Label congratsLabel = new Label(); + congratsLabel.textProperty().bind(lm.bindString("congratsLabel")); + congratsLabel.setStyle("-fx-font-size: 18"); + congratsLabel.setTextAlignment(TextAlignment.CENTER); + + HBox congratsBox = new HBox(); + congratsBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + congratsBox.setPadding(new Insets(8, 14, 8, 14)); + congratsBox.setAlignment(Pos.CENTER); + congratsBox.setMinHeight(65); + + congratsBox.getChildren().add(congratsLabel); + + Label statusLabelText = new Label(); + statusLabelText.textProperty().bind(lm.bindString("statusLabelText")); + statusLabelText.setStyle("-fx-font-size: 18"); + statusLabel = new Label(); + statusLabel.setStyle("-fx-font-size: 18"); + + HBox statusLabelBox = new HBox(); + statusLabelBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + statusLabelBox.setPadding(new Insets(8, 14, 8, 14)); + statusLabelBox.setAlignment(Pos.CENTER); + statusLabelBox.setMinHeight(65); + + statusLabelBox.getChildren().addAll(statusLabelText, statusLabel); + + Label moneyLabelText = new Label(); + moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText")); + moneyLabelText.setStyle("-fx-font-size: 18"); + moneyLabel = new Label(); + moneyLabel.setStyle("-fx-font-size: 18"); + + HBox moneyLabelBox = new HBox(); + moneyLabelBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + moneyLabelBox.setPadding(new Insets(8, 14, 8, 14)); + moneyLabelBox.setAlignment(Pos.CENTER); + moneyLabelBox.setMinHeight(65); + + moneyLabelBox.getChildren().addAll(moneyLabelText, moneyLabel); + + VBox centerPane = new VBox(); + centerPane.setPadding(new Insets(15, 12, 15, 12)); + centerPane.setAlignment(Pos.CENTER); + centerPane.setSpacing(10); + centerPane.setMaxWidth(700); + + + centerPane.getChildren().addAll(congratsBox, statusLabelBox, moneyLabelBox); + + quitButton = new Button(); + quitButton.textProperty().bind(lm.bindString("quitGame")); + quitButton.setPrefSize(200, 50); + quitButton.setStyle("-fx-text-fill: #303539;" + + "-fx-font-size: 18px;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + + "-fx-background-radius: 8;" + ); + + HBox quitButtonBox = new HBox(); + quitButtonBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + quitButtonBox.setPadding(new Insets(8, 14, 8, 14)); + quitButtonBox.setAlignment(Pos.CENTER); + + + quitButtonBox.getChildren().addAll(quitButton); + + newGameButton = new Button(); + newGameButton.textProperty().bind(lm.bindString("newGame")); + newGameButton.setPrefSize(200, 50); + newGameButton.setStyle("-fx-text-fill: #303539;" + + "-fx-font-size: 18px;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + + "-fx-background-radius: 8;" + ); + + HBox newGameButtonBox = new HBox(); + newGameButtonBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + newGameButtonBox.setPadding(new Insets(8, 14, 8, 14)); + newGameButtonBox.setAlignment(Pos.CENTER); + newGameButtonBox.getChildren().add(newGameButton); + + HBox buttons = new HBox(quitButtonBox, newGameButtonBox); + + Region spacerBottomLeft = new Region(); + Region spacerBottomRight = new Region(); + + HBox.setHgrow(spacerBottomLeft, Priority.ALWAYS); + HBox.setHgrow(spacerBottomRight, Priority.ALWAYS); + + HBox bottomPane = new HBox(spacerBottomLeft, buttons, spacerBottomRight); + + pane.setCenter(centerPane); + pane.setBottom(bottomPane); + + return pane; + } + + /** + * Sets the action to be run when clicking the quit button. + * + * @param action the action to run when clicking the quit button + */ + public void onQuitButton(Runnable action) { + quitButton.setOnAction(e -> action.run()); + } + + /** + * Sets the action to be run when clicking the New Game button. + * + * @param action the action to run when clicking the New Game button + */ + public void onNewGameButton(Runnable action) { + newGameButton.setOnAction(e -> action.run()); + } + + /** + * Returns the statusLabel. + * + * @return the statusLabel + */ + public Label getStatusLabel() { + return statusLabel; + } + + /** + * Returns the moneyLabel. + * + * @return the moneyLabel + */ + public Label getMoneyLabel() { + return moneyLabel; + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java new file mode 100644 index 0000000..dd9857b --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java @@ -0,0 +1,233 @@ +package no.ntnu.gruppe53.view; + +import java.util.Locale; +import java.util.function.Consumer; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.util.Subscription; +import no.ntnu.gruppe53.controller.GameController; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Stock; +import no.ntnu.gruppe53.service.LanguageManager; +import no.ntnu.gruppe53.service.LanguageOption; +import no.ntnu.gruppe53.service.LanguageRegistry; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) + */ + +/** + * A {@link HBox} view representing the footer/bottom to be used in a {@link BorderPane} layout. + * + *

Uses a {@link LanguageManager} to change text to target language dynamically.

+ */ +public class FooterBar extends HBox { + private final Button advanceWeekButton; + private final Label currentWeekLabel; + private final Label biggestLoserLabel; + private final Label biggestLoserPriceLabel; + private final Label biggestWinnerLabel; + private final Label biggestWinnerPriceLabel; + private Subscription biggestLoserSubscription; + private Subscription biggestWinnerSubscription; + + private final LanguageManager lm = LanguageManager.getInstance(); + + private final ComboBox languageBox; + + /** + * Constructs the layout of the view and its components. + * + *

Constructs a {@link ComboBox} to let the user switch language on the fly.

+ * + *

Sets the text of the UI elements dynamically.

+ */ + public FooterBar() { + this.setSpacing(10); + this.setPadding(new Insets(15, 12, 15, 12)); + this.setStyle("-fx-background-color: #00bcf0;" + + "-fx-border-color: #303539 transparent transparent transparent;" + + "-fx-border-width: 5 0 0 0;"); + + languageBox = new ComboBox<>(); + languageBox.getItems().addAll(LanguageRegistry.getLanguages()); + + Locale activeLocale = lm.getLocale(); + LanguageOption activeOption = LanguageRegistry.getLanguages().stream() + .filter(lang -> lang.locale().equals(activeLocale)) + .findFirst() + .orElse(LanguageRegistry.getDefaultLanguage()); + + languageBox.setValue(activeOption); + + // Listens for changes in language from other sources (e.g. startView) + lm.getBundleProperty().subscribe(bundle -> { + java.util.Locale currentLocale = lm.getLocale(); + + LanguageOption matchingOption = LanguageRegistry.getLanguages().stream() + .filter(lang -> lang.locale().equals(currentLocale)) + .findFirst() + .orElse(null); + + if (matchingOption != null && !matchingOption.equals(languageBox.getValue())) { + languageBox.setValue(matchingOption); + } + }); + + languageBox.setCellFactory(list -> new ListCell<>() { + private Subscription cellSub; + @Override + protected void updateItem(LanguageOption item, boolean empty) { + super.updateItem(item, empty); + if (cellSub != null) { + cellSub.unsubscribe(); + } + + if (empty || item == null) { + setText(null); + } else { + cellSub = lm.getBundleProperty().subscribe(bundle -> setText(item.name())); + } + } + }); + + languageBox.setButtonCell(languageBox.getCellFactory().call(null)); + + Label biggestLoserLabelText = new Label(); + biggestLoserLabelText.textProperty().bind(lm.bindString("biggestLoserLabelText")); + biggestLoserLabel = new Label(); + biggestLoserPriceLabel = new Label(); + biggestLoserPriceLabel.setStyle("-fx-text-fill: red;"); + + HBox loserBox = new HBox(); + loserBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + loserBox.setPadding(new Insets(8, 14, 8, 14)); + loserBox.setAlignment(Pos.CENTER); + + loserBox.getChildren().addAll(biggestLoserLabelText, biggestLoserLabel, biggestLoserPriceLabel); + + Label biggestWinnerLabelText = new Label(); + biggestWinnerLabelText.textProperty().bind(lm.bindString("biggestWinnerLabelText")); + biggestWinnerLabel = new Label(); + biggestWinnerPriceLabel = new Label(); + biggestWinnerPriceLabel.setStyle("-fx-text-fill: green;"); + + HBox winnerBox = new HBox(); + winnerBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + winnerBox.setPadding(new Insets(8, 14, 8, 14)); + winnerBox.setAlignment(Pos.CENTER); + + winnerBox.getChildren().addAll( + biggestWinnerLabelText, biggestWinnerLabel, biggestWinnerPriceLabel); + + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + HBox currentWeekBox = new HBox(); + currentWeekBox.setSpacing(10); + + currentWeekBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + currentWeekBox.setPadding(new Insets(8, 14, 8, 14)); + currentWeekBox.setAlignment(Pos.CENTER); + + Label currentWeekLabelText = new Label(); + currentWeekLabelText.textProperty().bind(lm.bindString("currentWeekLabelText")); + currentWeekLabel = new Label(); + currentWeekBox.getChildren().addAll(currentWeekLabelText, currentWeekLabel); + + advanceWeekButton = new Button(); + advanceWeekButton.textProperty().bind(lm.bindString("advanceWeekButton")); + advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + + this.getChildren().addAll( + languageBox, loserBox, winnerBox, spacer, currentWeekBox, advanceWeekButton); + } + + /** + * Sets the exchange and subscribes to week updates. + * + * @param exchange the {@link Exchange} used in the game + */ + public void setExchange(Exchange exchange) { + Subscription currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> { + if (exchange != null) { + currentWeekLabel.setText(newVal.toString()); + } else { + currentWeekLabel.setText("None"); + } + }); + + if (biggestLoserSubscription != null) { + biggestLoserSubscription.unsubscribe(); + } + + biggestLoserSubscription = exchange.getCurrentWeekProperty().subscribe(newWeek -> { + Stock loser = exchange.getLosers(1).getFirst(); + biggestLoserLabel.setText(loser.getSymbol() + " - " + loser.getCompany()); + biggestLoserPriceLabel.setText(" $" + loser.getLatestPriceChange()); + }); + + if (biggestWinnerSubscription != null) { + biggestWinnerSubscription.unsubscribe(); + } + + biggestWinnerSubscription = exchange.getCurrentWeekProperty().subscribe(newWeek -> { + Stock winner = exchange.getGainers(1).getFirst(); + biggestWinnerLabel.setText(winner.getSymbol() + " - " + winner.getCompany()); + biggestWinnerPriceLabel.setText(" $" + winner.getLatestPriceChange()); + }); + } + + /** + * Exposes the advanceWeekButton to {@link GameController} so it can + * advance the week of the {@link Exchange}. + * + * @param action the action to be performed when clicking the button + */ + public void onAdvanceButtonClick(Runnable action) { + advanceWeekButton.setOnAction(e -> action.run()); + } + + /** + * Exposes the LanguageOption ComboButton to the controller. + * + * @param action the action to be performed when selecting the language + */ + public void onLanguageChange(Consumer action) { + languageBox.setOnAction(e -> { + LanguageOption selected = languageBox.getValue(); + if (selected != null) { + action.accept(selected); + } + }); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java new file mode 100644 index 0000000..ce37a5d --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java @@ -0,0 +1,453 @@ +package no.ntnu.gruppe53.view; + +import java.math.RoundingMode; +import java.util.function.Consumer; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.Subscription; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.model.PurchaseCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; +import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) + */ + +/** + * Represents a view of the market of an {@link Exchange}, allowing the {@link Player} to see + * price history statistics in the form of a {@link LineChart} through the + * {@link StockLineChartView} class, as well as buying {@link Stock}s. + * + *

Extends borderpane to allow easy organizing of the internal layout.

+ * + *

Uses a {@link LanguageManager} to set text to the targer language dynamically.

+ */ +public class MarketView extends BorderPane { + private final LanguageManager lm = LanguageManager.getInstance(); + + private Button purchaseButton; + private Label selectedStock; + + // Dynamic Labels/Text + public TextField gross; + public TextField commission; + public TextField total; + private TextField searchField; + + // StockListView + private ListView stocksListView; + private FilteredList filteredStocksList; + + // Input + private Spinner quantitySpinner; + + private final StockLineChartView stockLineChartView; + private Subscription priceSubscription; + + /** + * Initializes the StockLineChartView graph and runs the view + * construction method and places it in the center of the + * BorderPane. + */ + public MarketView() { + stockLineChartView = new StockLineChartView(); + this.setCenter(centerPane()); + } + + /** + * Constructs the view and its components. + * + *

Subscribes to a stock's sales price to update the value immediately.

+ * + *

Subscribes to the current language to change text immediately.

+ * + *

Uses a cell factory to format the string found in the stocksListView.

+ * + * @return the constructed MarketView + */ + private VBox centerPane() { + VBox vbox = new VBox(); + + vbox.setPadding(new Insets(15, 12, 15, 12)); + vbox.setSpacing(10); + + // Blue Background color + vbox.setStyle("-fx-background-color: #00bcf0;"); + + Label titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("marketTitle")); + titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; "); + + VBox titleBox = new VBox(titleLabel); + titleBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + titleBox.setAlignment(Pos.CENTER); + titleBox.setPadding(new Insets(4)); + titleBox.setMaxWidth(200); + + // Stock Graph + stockLineChartView.getChart().setMinHeight(0); + stockLineChartView.getChart().setMaxHeight(Double.MAX_VALUE); + stockLineChartView.getChart().setMinWidth(0); + stockLineChartView.getChart().setMaxWidth(Double.MAX_VALUE); + stockLineChartView.getChart().getXAxis().labelProperty().bind(lm.bindString("xAxisLabel")); + stockLineChartView.getChart().getYAxis().labelProperty().bind(lm.bindString("yAxisLabel")); + + // List of stocks + stocksListView = new ListView<>(); + stocksListView.setMinHeight(0); + stocksListView.setMaxHeight(Double.MAX_VALUE); + stocksListView.setMinWidth(0); + stocksListView.setMaxWidth(Double.MAX_VALUE); + stocksListView.setStyle("-fx-background-color: transparent;"); + + stocksListView.setCellFactory(list -> new ListCell<>() { + private Subscription cellSubscription; + private Subscription langSubscription; + + @Override + protected void updateItem(Stock stock, boolean empty) { + super.updateItem(stock, empty); + + if (cellSubscription != null) { + cellSubscription.unsubscribe(); + cellSubscription = null; + } + + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } + + if (empty || stock == null) { + setText(null); + } else { + Runnable updateTextAction = () -> { + String template = lm.getString("stockCellFormat"); + setText(String.format(template, + stock.getSymbol(), + stock.getCompany(), + FormatBigDecimal.formatNumber(stock.salesPriceProperty().get()), + FormatBigDecimal.formatNumber(stock.getLatestPriceChange()), + FormatBigDecimal.formatNumber(stock.getHighestPrice()), + FormatBigDecimal.formatNumber(stock.getLowestPrice()) + )); + }; + + cellSubscription = stock.salesPriceProperty() + .subscribe(newPrice -> updateTextAction.run()); + langSubscription = lm.getBundleProperty() + .subscribe(bundle -> updateTextAction.run()); + + updateTextAction.run(); + } + } + }); + + HBox searchBox = new HBox(); + searchField = new TextField(); + searchField.promptTextProperty().bind(lm.bindString("search")); + searchBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + searchBox.setPadding(new Insets(8, 14, 8, 14)); + searchBox.setAlignment(Pos.CENTER_LEFT); + searchBox.setMaxWidth(Region.USE_PREF_SIZE); + searchBox.getChildren().addAll(searchField); + + + // Container for list of stocks + HBox stockListBox = new HBox(stocksListView); + stockListBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + // Container for graph of stocks + HBox stockChartBox = new HBox(stockLineChartView.getChart()); + stockChartBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + HBox stockBox = new HBox(stockListBox, stockChartBox); + stockBox.setSpacing(10); + + stockListBox.setPadding(new Insets(4)); + stockListBox.setMinHeight(0); + stockListBox.setMaxHeight(Double.MAX_VALUE); + stockListBox.setMinWidth(0); + stockListBox.setMaxWidth(Double.MAX_VALUE); + + VBox.setVgrow(stocksListView, Priority.ALWAYS); + HBox.setHgrow(stocksListView, Priority.ALWAYS); + VBox.setVgrow(stockLineChartView.getChart(), Priority.ALWAYS); + HBox.setHgrow(stockLineChartView.getChart(), Priority.ALWAYS); + VBox.setVgrow(stockListBox, Priority.ALWAYS); + HBox.setHgrow(stockListBox, Priority.ALWAYS); + VBox.setVgrow(stockChartBox, Priority.ALWAYS); + HBox.setHgrow(stockChartBox, Priority.ALWAYS); + VBox.setVgrow(stockBox, Priority.ALWAYS); + HBox.setHgrow(stockBox, Priority.ALWAYS); + + HBox selectedStockBox = new HBox(); + + Label selectedStockLabel = new Label(); + selectedStockLabel.textProperty().bind(lm.bindString("selectedStock")); + selectedStock = new Label(""); + + selectedStockBox.getChildren().addAll(selectedStockLabel, selectedStock); + + Label quantityLabel = new Label(); + quantityLabel.textProperty().bind(lm.bindString("purchaseQuantity")); + quantitySpinner = new Spinner<>(1, 1_000_000, 1); + quantitySpinner.setEditable(true); + + HBox quantityBox = new HBox(); + quantityBox.getChildren().addAll(quantityLabel, quantitySpinner); + + Label grossLabel = new Label(); + grossLabel.textProperty().bind(lm.bindString("purchaseGross")); + gross = new TextField(); + gross.setEditable(false); + gross.setMaxWidth(200); + Label commissionLabel = new Label(); + commissionLabel.textProperty().bind(lm.bindString("purchaseCommission")); + commission = new TextField(); + commission.setEditable(false); + commission.setMaxWidth(200); + + HBox calculationBox = new HBox(); + calculationBox.getChildren().addAll(grossLabel, gross, commissionLabel, commission); + + Label totalLabel = new Label(); + totalLabel.textProperty().bind(lm.bindString("purchaseTotal")); + total = new TextField(""); + total.setEditable(false); + total.setMaxWidth(200); + + HBox totalBox = new HBox(); + totalBox.getChildren().addAll(totalLabel, total); + + purchaseButton = new Button(); + purchaseButton.textProperty().bind(lm.bindString("purchaseButton")); + + VBox purchaseBox = new VBox(5); + + purchaseBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + purchaseBox.setPadding(new Insets(8, 14, 8, 14)); + purchaseBox.setAlignment(Pos.CENTER_LEFT); + + purchaseBox.getChildren().addAll( + selectedStockBox, quantityBox, calculationBox, totalBox, purchaseButton); + + + + vbox.getChildren().addAll(titleBox, searchBox, + stockBox, + purchaseBox); + + return vbox; + } + + /** + * Sets and updates subscription to the selected stock's sales price. + * + * @param stock the selected stock + * @param priceChangeAction the action to run on price change (updates the sales price) + */ + public void setupPriceSubscription(Stock stock, Consumer priceChangeAction) { + if (priceSubscription != null) { + priceSubscription.unsubscribe(); + priceSubscription = null; + } + + if (stock != null) { + priceSubscription = stock.salesPriceProperty().subscribe(newPrice -> { + priceChangeAction.accept(stock); + }); + } + } + + /** + * Updates chart data in the StockLineChartView. + * + * @param series the {@link javafx.scene.chart.XYChart.Series} data for the chart + * @param companyName the name of the company of the selected stock + */ + public void updateChart(XYChart.Series series, String companyName) { + stockLineChartView.setSeries(series, companyName); + } + + /** + * Unsubscribes from the previous stocks sales price and clears the LineChart. + */ + public void clearChart() { + if (priceSubscription != null) { + priceSubscription.unsubscribe(); + priceSubscription = null; + } + stockLineChartView.getChart().getData().clear(); + } + + /** + * Sets the action to be run on selecting a stock in stockListView. + * + * @param action the action to run when selecting a stock + */ + public void onStockSelection(Consumer action) { + stocksListView.getSelectionModel().selectedItemProperty().subscribe(action); + } + + /** + * Sets the action to be run when clicking the purchase button. + * + * @param action the action to run when clicking the purchase button + */ + public void onPurchaseButton(Runnable action) { + purchaseButton.setOnAction(e -> action.run()); + } + + /** + * Sets the action to be run when changing the value of the quantitySpinner. + * + * @param action the action to run when using the quantitySpinner + */ + public void onQuantitySelect(Runnable action) { + quantitySpinner.valueProperty().addListener((obs, oldValue, newValue) -> { + action.run(); + }); + } + + /** + * Sets the exchange to use for the view. + * + *

Uses first a {@link FilteredList} to account for what the user has searched for.

+ * + *

Then utilises a {@link SortedList} to sort the stocks in the exchange alphabetically + * on their symbol/ticker.

+ * + * @param exchange the exchange to be used for the view + */ + public void setExchange(Exchange exchange) { + if (exchange != null) { + filteredStocksList = new FilteredList<>(exchange.getObservableStocks()); + + SortedList sortedStocks = new SortedList<>(filteredStocksList); + sortedStocks.setComparator((s1, s2) -> s1.getSymbol().compareToIgnoreCase(s2.getSymbol())); + + stocksListView.setItems(sortedStocks); + + searchField.textProperty().addListener((observable, oldText, newText) -> { + String search = newText == null ? "" : newText.toLowerCase(); + + filteredStocksList.setPredicate(stock -> search.isBlank() + || stock.getSymbol().toLowerCase().contains(search) + || stock.getCompany().toLowerCase().contains(search) + ); + }); + } + } + + /** + * Sets the PurchaseCalculator to use for the view. + * + *

Uses a {@link Subscription} to observe the calculations made by the current + * Stock and Quantity selected.

+ * + * @param purchaseCalculator the purchaseCalculator to be used for the view + */ + public void setPurchaseCalculator(PurchaseCalculator purchaseCalculator) { + Subscription grossSubscription = purchaseCalculator.getGrossProperty().subscribe(newVal -> { + if (newVal != null) { + gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + gross.setText(""); + } + }); + + Subscription commissionSubscription = + purchaseCalculator.getCommissionProperty().subscribe(newVal -> { + if (newVal != null) { + commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + commission.setText(""); + } + }); + + Subscription totalSubscription = purchaseCalculator.getTotalProperty().subscribe(newVal -> { + if (newVal != null) { + total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + total.setText(""); + } + }); + } + + /** + * Returns the selected stock in the stocksListView. + * + * @return the selected stock in the stocksListView + */ + public Stock getSelectedStock() { + return stocksListView.getSelectionModel().getSelectedItem(); + } + + /** + * Returns the numeric value of the quantitySpinner for purchasing {@link Share}s in a stock. + * + * @return an integer of the amount of shares to buy + */ + public int getQuantityPurchase() { + return quantitySpinner.getValue(); + } + + /** + * Sets the company name of the selected stock in the stocksListView. + * + * @param selectedStockString the company name of the selected stock + */ + public void setSelectedStockLabel(String selectedStockString) { + selectedStock.setText(selectedStockString); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java new file mode 100644 index 0000000..838e521 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java @@ -0,0 +1,240 @@ +package no.ntnu.gruppe53.view; + +import java.math.RoundingMode; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.util.Subscription; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.service.LanguageManager; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) +*/ + +/** + * Represents the top bar of the views. + * + *

Provides the user with the ability to switch between views on the fly by + * clicking on the buttons representing the desired view.

+ * + *

The {@link no.ntnu.gruppe53.controller.GameController} handles + * the actions for the buttons.

+ * + *

Uses a {@link LanguageManager} to set text of UI elements dynamically.

+ */ +public class NavigationBar extends HBox { + + private final Button marketButton; + private final Button portfolioButton; + private final Button historyButton; + private final Button endGameButton; + private Subscription netWorthSubscription; + + private Player player; + + // Dynamic Labels + private final Label playerLabel; + private final Label statusLabel; + private final Label netWorthLabel; + private final Label moneyLabel; + + /** + * Constructs the UI elements of the navigationBar. + * + *

Binds the language manager to the textProperty of the + * UI elements to dynamically change the text.

+ */ + public NavigationBar() { + this.setPadding(new Insets(15, 12, 15, 12)); + this.setSpacing(10); + // Blue Background color + this.setStyle("-fx-background-color: #00bcf0;" + + "-fx-border-color: transparent transparent #303539 transparent;" + + "-fx-border-width: 0 0 5 0;"); + + marketButton = new Button(); + LanguageManager lm = LanguageManager.getInstance(); + marketButton.textProperty().bind(lm.bindString("marketButton")); + marketButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + portfolioButton = new Button(); + portfolioButton.textProperty().bind(lm.bindString("portfolioButton")); + portfolioButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + historyButton = new Button(); + historyButton.textProperty().bind(lm.bindString("historyButton")); + historyButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + HBox playerBox = new HBox(); + + Label playerLabelText = new Label(); + playerLabelText.textProperty().bind(lm.bindString("navPlayerLabelText")); + playerLabel = new Label("Player"); + + playerBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + playerBox.setPadding(new Insets(8, 14, 8, 14)); + playerBox.setAlignment(Pos.CENTER); + + playerBox.getChildren().addAll(playerLabelText, playerLabel); + + HBox statusBox = new HBox(); + + Label statusLabelText = new Label(); + statusLabelText.textProperty().bind(lm.bindString("statusLabelText")); + statusLabel = new Label(); + + statusBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + statusBox.setPadding(new Insets(8, 14, 8, 14)); + statusBox.setAlignment(Pos.CENTER); + + statusBox.getChildren().addAll(statusLabelText, statusLabel); + + HBox moneyBox = new HBox(); + + Label moneyLabelText = new Label(); + moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText")); + moneyLabel = new Label(); + + moneyBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + moneyBox.setPadding(new Insets(8, 14, 8, 14)); + moneyBox.setAlignment(Pos.CENTER); + moneyBox.getChildren().addAll(moneyLabelText, moneyLabel); + + HBox networthBox = new HBox(); + + Label networthLabelText = new Label(); + networthLabelText.textProperty().bind(lm.bindString("navNetWorthLabelText")); + netWorthLabel = new Label(); + + networthBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + networthBox.setPadding(new Insets(8, 14, 8, 14)); + networthBox.setAlignment(Pos.CENTER); + networthBox.getChildren().addAll(networthLabelText, netWorthLabel); + + endGameButton = new Button(); + endGameButton.textProperty().bind(lm.bindString("endGameButton")); + endGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + + this.getChildren().addAll( + marketButton, portfolioButton, historyButton, + spacer, playerBox, statusBox, moneyBox, networthBox, endGameButton); + } + + /** + * Exposes the marketButton to the GameController. + * + * @param action the action to be run when clicking the button + */ + public void onMarketButtonClick(Runnable action) { + marketButton.setOnAction(e -> action.run()); + } + + /** + * Exposes the portfolioButton to the GameController. + * + * @param action the action to be run when clicking the button + */ + public void onPortfolioButtonClick(Runnable action) { + portfolioButton.setOnAction(e -> action.run()); + } + + /** + * Exposes the historyButton to the GameController. + * + * @param action the action to be run when clicking the button + */ + public void onHistoryButtonClick(Runnable action) { + historyButton.setOnAction(e -> action.run()); + } + + /** + * Exposes the endGameButton to the GameController. + * + * @param action the action to be run when clicking the button + */ + public void onEndGameButtonClick(Runnable action) { + endGameButton.setOnAction(e -> action.run()); + } + + /** + * Binds a subscription/observer to a {@link Player}. + * + *

It specifically listens to the players money, net worth and name.

+ * + * @param player the player object to be observed + */ + public void bindPlayer(Player player) { + this.player = player; + + if (netWorthSubscription != null) { + netWorthSubscription.unsubscribe(); + } + + if (player != null) { + playerLabel.setText(player.getName()); + netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> { + + if (newVal != null) { + netWorthLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + netWorthLabel.setText("$0.00"); + } + }); + + Subscription moneySubscription = player.getMoneyProperty().subscribe(newVal -> { + if (newVal != null) { + moneyLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + moneyLabel.setText("$0.00"); + } + }); + + } + } + + /** + * Binds a subscription/observer to a {@link Exchange}. + * + *

It specifically listens to week changes so that it can update the player status.

+ * + * @param exchange the exchange object to be observed + */ + public void bindExchange(Exchange exchange) { + Subscription currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> { + statusLabel.setText(player.getStatus(exchange)); + }); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java new file mode 100644 index 0000000..046fcb1 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java @@ -0,0 +1,114 @@ +package no.ntnu.gruppe53.view; + +import java.util.Optional; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.service.LanguageManager; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) + */ + +/** + * Represents a prompt for the {@link Player}'s name. + */ +public class PlayerNameChooserView { + private final LanguageManager lm = LanguageManager.getInstance(); + Dialog dialog; + + /** + * Returns the dialog where the player can input a name of maximum 50 characters. + * + *

A {@link TextFormatter} is used to set the maximum amount + * of characters for the player name.

+ * + *

Uses a {@link LanguageManager} to determine text language.

+ * + * @return the dialog for player name input + */ + public Optional getDialog() { + dialog = new Dialog<>(); + + // Sets dialog title and header text + dialog.setTitle(null); + dialog.setHeaderText(null); + + // Sets the dimensions for the dialog pane + dialog.setResizable(true); + + dialog.getDialogPane().setPrefWidth(320); + dialog.getDialogPane().setPrefHeight(50); + + ButtonType confirmButton = + new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().add(confirmButton); + Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton); + + actualConfirmButton.textProperty().bind(lm.bindString("playerNameConfirmButton")); + + TextField nameField = new TextField(); + + nameField.setEditable(true); + + int maxLength = 50; + + // Prevents player name over 50 characters + + TextFormatter formatter = new TextFormatter<>(change -> { + + String newText = change.getControlNewText(); + + if (newText.length() <= maxLength) { + return change; + } + + return null; + + }); + + nameField.setTextFormatter(formatter); + + // Adds a counter for max characters + Label counterLabel = new Label("0 / 50"); + + nameField.textProperty().addListener((obs, oldVal, newVal) -> { + counterLabel.setText(newVal.length() + " / 50"); + }); + + Label infoLabel = new Label(); + infoLabel.textProperty().bind(lm.bindString("playerNameInfo")); + + VBox layout = new VBox(15); + + HBox hbox = new HBox(15); + + layout.setPadding(new Insets(10)); + + hbox.getChildren().addAll(nameField, counterLabel); + + layout.getChildren().addAll(infoLabel, hbox); + + dialog.getDialogPane().setContent(layout); + + dialog.setResultConverter(button -> { + if (button == confirmButton) { + return nameField.getText(); + } + + return null; + }); + + return dialog.showAndWait(); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java new file mode 100644 index 0000000..7962367 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java @@ -0,0 +1,116 @@ +package no.ntnu.gruppe53.view; + +import java.math.BigDecimal; +import java.util.Optional; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextFormatter; +import javafx.scene.layout.VBox; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.service.LanguageManager; + +/** + * Represents the prompt for the {@link Player}'s starting money. + * + *

Uses a {@link LanguageManager} to set text to target language.

+ */ +public class PlayerStartingMoneyChooserView { + Dialog dialog = new Dialog<>(); + private final LanguageManager lm = LanguageManager.getInstance(); + + /** + * Returns the dialog for the player's starting money. + * + *

Uses a {@link TextFormatter} and listener for value integrity.

+ * + *

Uses a {@link SpinnerValueFactory} to set min, max, default, + * and step size values for the spinner.

+ * + * @return the dialog for player's starting money + */ + public Optional getDialog() { + // Sets dialog title and header text + dialog.setTitle(null); + dialog.setHeaderText(null); + + // Sets the dimensions for the dialog pane + dialog.setResizable(true); + + dialog.getDialogPane().setPrefWidth(320); + dialog.getDialogPane().setPrefHeight(50); + + ButtonType confirmButton = + new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().add(confirmButton); + + Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton); + + actualConfirmButton.textProperty().bind(lm.bindString("playerStartingMoneyConfirmButton")); + + Spinner spinner = new Spinner<>(); + + // Sets allowed values and step size for the spinner + SpinnerValueFactory.DoubleSpinnerValueFactory valueFactory = + new SpinnerValueFactory.DoubleSpinnerValueFactory( + 1000.0, // min + 1000000.0, // max + 5000.0, // initial value + 100.0 // step-size + ); + + spinner.setValueFactory(valueFactory); + + spinner.setEditable(true); + + // Formats the text to only allow numbers, and doubles preceded by a number + TextFormatter formatter = new TextFormatter<>(change -> { + + String newText = change.getControlNewText(); + + if (newText.matches("\\d+(\\.\\d*)?")) { + return change; + } + + return null; + }); + + spinner.focusedProperty().addListener((obs, oldVal, newVal) -> { + + if (!newVal) { + spinner.increment(0); + } + }); + + spinner.getEditor().setTextFormatter(formatter); + + spinner.setPrefWidth(200); + + Label infoLabel = new Label(); + infoLabel.textProperty().bind(lm.bindString("playerStartingMoneyInfoLabel")); + + VBox layout = new VBox(15); + + layout.setPadding(new Insets(10)); + + layout.getChildren().addAll(infoLabel, spinner); + + dialog.getDialogPane().setContent(layout); + + dialog.setResultConverter(button -> { + + if (button == confirmButton) { + return BigDecimal.valueOf(spinner.getValue()); + } + + return null; + }); + + return dialog.showAndWait(); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java new file mode 100644 index 0000000..416d5c0 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java @@ -0,0 +1,410 @@ +package no.ntnu.gruppe53.view; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.function.Consumer; +import javafx.collections.transformation.FilteredList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.util.Subscription; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.model.SaleCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) + */ + +/** + * Represent a view of the {@link Player}s {@link no.ntnu.gruppe53.model.Portfolio}. + * + *

Lets the player view their current {@link Share} assets, see their + * current sale value, whether the value has gone up or down since they purchased, + * and the ability to sell the share for money.

+ * + *

Extends the {@link BorderPane}

for easy layout setup. + * + *

Events are handled by the {@link no.ntnu.gruppe53.controller.GameController}.

+ * + *

Uses a {@link LanguageManager} to dynamically set text based on target language.

+ */ +public class PortfolioView extends BorderPane { + private final LanguageManager lm = LanguageManager.getInstance(); + + private Button sellButton; + + // Dynamic Labels + private Label selectedShare; + private TextField gross; + private TextField commission; + private TextField tax; + private TextField total; + private TextField profit; + private TextField searchField; + + // PortfolioListView + private ListView portfolioListView; + private FilteredList filteredPortfolioList; + + /** + * Builds the view components and places them in the center pane. + */ + public PortfolioView() { + this.setCenter(centerPane()); + } + + /** + * Constructs the view of the portfolio. + * + *

Subscribes to shares for sales price updates.

+ * + *

Uses a cell factory to format the displayed string representing each share. + * The sell factory has the text dynamically set by the language manager.

+ * + * @return a VBox of the portfolio view + */ + private VBox centerPane() { + VBox vbox = new VBox(); + vbox.setPadding(new Insets(15, 12, 15, 12)); + vbox.setSpacing(10); + // Blue Background color + vbox.setStyle("-fx-background-color: #00bcf0; "); + + Label titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("portfolioTitle")); + titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;"); + + VBox titleBox = new VBox(titleLabel); + titleBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + titleBox.setAlignment(Pos.CENTER); + titleBox.setPadding(new Insets(4)); + titleBox.setMaxWidth(200); + + portfolioListView = new ListView<>(); + portfolioListView.setMinHeight(0); + portfolioListView.setMaxHeight(Double.MAX_VALUE); + + portfolioListView.setCellFactory(list -> new ListCell<>() { + private Subscription cellSubscription; + private Subscription langSubscription; + + @Override + protected void updateItem(Share item, boolean empty) { + super.updateItem(item, empty); + + if (cellSubscription != null) { + cellSubscription.unsubscribe(); + cellSubscription = null; + } + + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } + + if (empty || item == null || item.getStock() == null) { + setText(null); + setGraphic(null); + } else { + setText(null); + + Runnable updateGraphicAction = () -> { + BigDecimal currentPrice = item.getStock().salesPriceProperty().get(); + setGraphic(createColoredShareText(item, currentPrice)); + }; + + cellSubscription = item.getStock().salesPriceProperty().subscribe(currentPrice -> { + updateGraphicAction.run(); + }); + + langSubscription = lm.getBundleProperty().subscribe(bundle -> { + updateGraphicAction.run(); + }); + + updateGraphicAction.run(); + } + } + }); + + HBox searchBox = new HBox(); + searchField = new TextField(); + searchField.promptTextProperty().bind(lm.bindString("search")); + searchBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + searchBox.setPadding(new Insets(8, 14, 8, 14)); + searchBox.setAlignment(Pos.CENTER_LEFT); + searchBox.setMaxWidth(Region.USE_PREF_SIZE); + searchBox.getChildren().addAll(searchField); + + VBox portfolioListBox = new VBox(portfolioListView); + portfolioListBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + portfolioListBox.setPadding(new Insets(4)); + portfolioListBox.setMinHeight(0); + portfolioListBox.setMaxHeight(Double.MAX_VALUE); + + // Ensures the portfoliolist grows to fit the available screenspace + VBox.setVgrow(portfolioListView, Priority.ALWAYS); + VBox.setVgrow(portfolioListBox, Priority.ALWAYS); + + HBox selectedShareBox = new HBox(); + + Label selectedShareLabel = new Label(); + selectedShareLabel.textProperty().bind(lm.bindString("selectedShare")); + selectedShare = new Label(); + + selectedShareBox.getChildren().addAll(selectedShareLabel, selectedShare); + + Label grossLabel = new Label(); + grossLabel.textProperty().bind(lm.bindString("portfolioGross")); + gross = new TextField(); + gross.setEditable(false); + gross.setMaxWidth(200); + Label commissionLabel = new Label(); + commissionLabel.textProperty().bind(lm.bindString("portfolioCommission")); + commission = new TextField(); + commission.setEditable(false); + commission.setMaxWidth(200); + Label taxLabel = new Label(); + taxLabel.textProperty().bind(lm.bindString("portfolioTax")); + tax = new TextField(); + tax.setEditable(false); + tax.setMaxWidth(200); + + HBox calculationBox = new HBox(); + calculationBox.getChildren().addAll( + grossLabel, gross, commissionLabel, commission, taxLabel, tax); + + Label totalLabel = new Label(); + totalLabel.textProperty().bind(lm.bindString("portfolioTotal")); + total = new TextField(); + total.setEditable(false); + total.setMaxWidth(200); + + Label profitLabel = new Label(); + profitLabel.textProperty().bind(lm.bindString("portfolioProfit")); + profit = new TextField(); + profit.setEditable(false); + profit.setMaxWidth(200); + + HBox totalBox = new HBox(); + totalBox.getChildren().addAll(totalLabel, total, profitLabel, profit); + + sellButton = new Button(); + sellButton.textProperty().bind(lm.bindString("sellButton")); + + VBox saleBox = new VBox(5); + + saleBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + saleBox.setPadding(new Insets(8, 14, 8, 14)); + saleBox.setAlignment(Pos.CENTER_LEFT); + + saleBox.getChildren().addAll(selectedShareBox, calculationBox, totalBox, sellButton); + + + vbox.getChildren().addAll(titleBox, searchBox, + portfolioListBox, + saleBox); + return vbox; + } + + /** + * Exposes the sell button to the GameController. + * + * @param action the action to be run when pressing the sell button + */ + public void onSellButton(Runnable action) { + sellButton.setOnAction(e -> action.run()); + } + + /** + * Sets the action to be run on selecting a share in portfolioListView. + * + * @param action the action to run when selecting a share + */ + public void onShareSelection(Consumer action) { + portfolioListView.getSelectionModel().selectedItemProperty().subscribe(action); + } + + /** + * Gets the selected share in the portfolioListView. + * + * @return the currently selected share + */ + public Share getSelectedShare() { + return portfolioListView.getSelectionModel().getSelectedItem(); + } + + /** + * Sets the player to be observed. + * + *

The values observed are the players portfolio and name.

+ * + *

Uses a {@link FilteredList} to get and filter the shares and + * binds it to the portfolioListView.

+ * + * @param player the player to be observed + */ + public void setPlayer(Player player) { + if (player != null && player.getPortfolio() != null) { + filteredPortfolioList = new FilteredList<>(player.getPortfolio().getShares()); + + portfolioListView.setItems(filteredPortfolioList); + + searchField.textProperty().addListener((observable, oldText, newText) -> { + String search = newText == null ? "" : newText.toLowerCase(); + + filteredPortfolioList.setPredicate(share -> search.isBlank() + || share.getStock().getSymbol().toLowerCase().contains(search) + || share.getStock().getCompany().toLowerCase().contains(search) + ); + }); + + } else { + + portfolioListView.setItems(null); + } + } + + /** + * Sets the share name of the selected share in the portfolioListView. + * + * @param selectedShareString the share name of the selected share + */ + public void setSelectedShareLabel(String selectedShareString) { + selectedShare.setText(selectedShareString); + } + + /** + * Sets the SaleCalculator to use for the view. + * + *

Uses a {@link Subscription} to observe the calculations made by the current + * Share selected.

+ * + * @param saleCalculator the SaleCalculator to be used for the view + */ + public void setSaleCalculator(SaleCalculator saleCalculator) { + Subscription grossSubscription = + saleCalculator.getGrossProperty().subscribe(newVal -> { + if (newVal != null) { + gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + gross.setText(""); + } + }); + + Subscription commissionSubscription = + saleCalculator.getCommissionProperty().subscribe(newVal -> { + if (newVal != null) { + commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + commission.setText(""); + } + }); + + Subscription taxSubscription = saleCalculator.getTaxProperty().subscribe(newVal -> { + if (newVal != null) { + tax.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN).abs()); + } else { + tax.setText(""); + } + }); + + Subscription totalSubscription = saleCalculator.getTotalProperty().subscribe(newVal -> { + if (newVal != null) { + total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + total.setText(""); + } + }); + + Subscription profitSubscription = saleCalculator.getProfitProperty().subscribe(newVal -> { + if (newVal != null) { + profit.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN)); + } else { + profit.setText(""); + } + }); + } + + /** + * Formats the text representation of the share in the portfolioListView + * and applies a color to it based on its current sales price compared + * to the buy price of the share. + * + * @param share the share to be formatted + * @param currentPrice current price of the stock the share belongs to + * @return a {@link TextFlow} representation of the share + */ + private TextFlow createColoredShareText(Share share, BigDecimal currentPrice) { + String text = share.getStock().getSymbol() + + " - " + + share.getStock().getCompany() + + " | " + + lm.getString("portfolioShareQuantity") + + share.getQuantity() + + " | " + + lm.getString("portfolioBuyPrice") + + "$" + + FormatBigDecimal.formatNumber(share.getPurchasePrice()) + + " | " + + lm.getString("portfolioCurrentPrice") + + "$"; + + Text t1 = new Text(text); + Text tvalue = new Text(FormatBigDecimal.formatNumber(currentPrice)); + + BigDecimal buyPrice = share.getPurchasePrice(); + if (buyPrice != null && currentPrice != null) { + int compare = currentPrice.compareTo(buyPrice); + + if (compare > 0) { + tvalue.setStyle("-fx-fill: #2ecc71; -fx-font-weight: bold;"); + } else if (compare < 0) { + tvalue.setStyle("-fx-fill: #e74c3c; -fx-font-weight: bold;"); + } else { + tvalue.setStyle("-fx-fill: #000000;"); + } + } + + return new TextFlow(t1, tvalue); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java new file mode 100644 index 0000000..264fa74 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java @@ -0,0 +1,88 @@ +package no.ntnu.gruppe53.view; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import javafx.util.Subscription; +import no.ntnu.gruppe53.controller.StartViewController; +import no.ntnu.gruppe53.controller.ViewController; +import no.ntnu.gruppe53.service.LanguageManager; + +/** + * Represents the first page seen by the user of the application. + * + *

Uses a {@link LanguageManager} for dynamically setting button and label text. + * Subscribes to changes in language set by the manager.

+ */ +public class StartView extends VBox { + private final Button newGameButton; + private final Button languageButton; + private final Button quitButton; + + /** + * Constructs the view and sets parameters to be exposed for the {@link StartViewController}. + * + * @param vm the {@link ViewController} for the view + */ + public StartView(ViewController vm) { + this.setAlignment(Pos.CENTER); + this.setSpacing(20); + + this.setStyle("-fx-background-color: #00bcf0;"); + + Image logo = new Image(getClass().getClassLoader().getResourceAsStream("Millions-logo.png")); + ImageView logoView = new ImageView(logo); + logoView.setFitWidth(300); + logoView.setPreserveRatio(true); + + LanguageManager lm = LanguageManager.getInstance(); + newGameButton = new Button(); + newGameButton.textProperty().bind(lm.bindString("newGame")); + + languageButton = new Button(); + languageButton.setPrefWidth(200); + languageButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + Subscription languageSubscription = lm.getBundleProperty().subscribe(bundle -> { + languageButton.setText(lm.getString("languageButtonLabel")); + }); + + quitButton = new Button(); + quitButton.textProperty().bind(lm.bindString("quitGame")); + + newGameButton.setPrefWidth(200); + newGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + quitButton.setPrefWidth(200); + quitButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + + this.getChildren().addAll(logoView, newGameButton, languageButton, quitButton); + } + + /** + * Sets the method to be run when the new game button is pressed. + * + * @param action the method to execute on button press + */ + public void setOnNewGame(Runnable action) { + newGameButton.setOnAction(e -> action.run()); + } + + /** + * Sets the method to be run when the languageButton is pressed. + * + * @param action the method to execute on button press + */ + public void setOnLanguage(Runnable action) { + languageButton.setOnAction(e -> action.run()); + } + + /** + * Sets the method to be run when the quit game button is pressed. + * + * @param action the method to execute on button press + */ + public void setOnQuit(Runnable action) { + quitButton.setOnAction(e -> action.run()); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java new file mode 100644 index 0000000..048d5b9 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java @@ -0,0 +1,128 @@ +package no.ntnu.gruppe53.view; + +import java.util.List; +import javafx.geometry.Side; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import no.ntnu.gruppe53.model.Stock; + +/** + * Represents a {@link LineChart} view of a {@link Stock}. + * + *

Y-axis: Price as a {@code double} value. Represented by a {@link NumberAxis}

+ * + *

X-axis: Week as an {@code int} value. Represented by a {@code NumberAxis}

+ */ + +public class StockLineChartView { + private final NumberAxis xaxis = new NumberAxis(); + + private final LineChart chart; + + /** + * Sets the settings for the {@code LineChart}. + * + *

Sets autoscale, stepsize and manual values for the range x- and y-axis, as well + * as the labels.

+ * + *

Initializes the LineChart.

+ */ + public StockLineChartView() { + xaxis.setAutoRanging(false); + NumberAxis yaxis = new NumberAxis(); + yaxis.setAutoRanging(true); + yaxis.setForceZeroInRange(false); + + xaxis.setLowerBound(1); + + xaxis.setLabel("Week"); + yaxis.setLabel("Price"); + + chart = new LineChart<>(xaxis, yaxis); + } + + /** + * Returns the constructed LineChart. + * + * @return the current LineChart + */ + public LineChart getChart() { + return chart; + } + + /** + * Calculates the max x-axis value for a {@link XYChart.Series}. + * + * @param series the XYChart series to determine max x-axis value from + * @return the maximum x-axis value in the XYChart.Series + */ + private double calculatexAxisMax(XYChart.Series series) { + return series.getData().stream() + .mapToDouble(data -> data.getXValue() + .doubleValue()) + .max() + .orElse(0); + } + + /** + * Calculates the max x-axis value for a {@code List}. + * + * @param seriesList the list of XYChart.Series to determine max x-axis value from + * @return the maximum x-axis value in the List of XYChart.Series + */ + private double calculatexAxisMax(List> seriesList) { + double xmax = 0; + + for (XYChart.Series s : seriesList) { + double tempMax = calculatexAxisMax(s); + + if (tempMax > xmax) { + xmax = tempMax; + } + } + + return xmax; + } + + /** + * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}. + * + * @param series the XYChart.Series containing the x- and y-values for the LineChart + * @param title the name of the company the stock represents + */ + + public void setSeries(XYChart.Series series, String title) { + chart.getData().clear(); + + chart.setTitle(title); + chart.setLegendSide(Side.RIGHT); + + double xmax = calculatexAxisMax(series); + xaxis.setUpperBound(xmax); + + chart.getData().add(series); + } + + /** + * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}. + * + *

Here the series values are given as a list of {@code XYChart.Series} objects.

+ * + * @param seriesList the list of XYChart.Series containing the x- and y-values for each stock + */ + public void setSeries(List> seriesList) { + chart.getData().clear(); + + chart.setTitle("Price History"); + chart.setLegendSide(Side.RIGHT); + + double xax = calculatexAxisMax(seriesList); + + if (xax != 0) { + xaxis.setUpperBound(xax); + } + + chart.getData().addAll(seriesList); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java new file mode 100644 index 0000000..75ea202 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java @@ -0,0 +1,235 @@ +package no.ntnu.gruppe53.view; + +import java.math.BigDecimal; +import javafx.collections.transformation.FilteredList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.Subscription; +import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.model.Purchase; +import no.ntnu.gruppe53.model.Sale; +import no.ntnu.gruppe53.model.SaleCalculator; +import no.ntnu.gruppe53.model.Transaction; +import no.ntnu.gruppe53.model.TransactionArchive; +import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; + +/* +Yellow Color, Hex: #ffe556 RGB(255, 229, 86) +Blue Color, Hex: #00bcf0 RGB(0, 188, 240) +Black Color, Hex: #303539 RGB(48, 53, 57) + */ + +/** + * Represents a view of all the {@link Purchase} and {@link Sale} + * {@link Transaction}s a {@link Player} has committed in a game. + * + *

Extends the {@link BorderPane} for easy layout setup.

+ */ +public class TransactionArchiveView extends BorderPane { + private final LanguageManager lm = LanguageManager.getInstance(); + + private ListView historyListView; + private FilteredList filteredTransactionList; + + private TextField searchField; + + /** + * Builds the view components and places them in the center pane. + */ + public TransactionArchiveView() { + this.setCenter(centerPane()); + } + + /** + * Constructs the view of the transaction archive. + * + *

Builds the necessary components and places them in a {@link VBox}.

+ * + *

Uses a cell factory to format the string shown

+ * + *

Assigns red text to purchases, and green text to sales.

+ * + *

Uses the formatBigDecimal method to convert the BigDecimal to a + * string with 2 decimals.

+ * + *

Uses a {@link LanguageManager} to dynamically set text based on target language.

+ * + * @return a VBox of the constructed view of the transaction archive + */ + private VBox centerPane() { + VBox vbox = new VBox(10); + vbox.setPadding(new Insets(15, 12, 15, 12)); + vbox.setStyle("-fx-background-color: #00bcf0; "); + + Label titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("historyTitle")); + titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;"); + + VBox titleBox = new VBox(titleLabel); + titleBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + titleBox.setAlignment(Pos.CENTER); + titleBox.setPadding(new Insets(4)); + titleBox.setMaxWidth(300); + + historyListView = new ListView<>(); + historyListView.setMinHeight(0); + historyListView.setMaxHeight(Double.MAX_VALUE); + + historyListView.setCellFactory(list -> new ListCell<>() { + private Subscription langSubscription; + + @Override + protected void updateItem(Transaction item, boolean empty) { + super.updateItem(item, empty); + + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } + + if (empty || item == null) { + setText(null); + setStyle(""); + } else { + Runnable updateTextAction = () -> { + String typeStr; + BigDecimal price = BigDecimal.ZERO; + BigDecimal profit = BigDecimal.ZERO; + + // Changed to allow for more transaction types in the future + if (item instanceof Purchase) { + typeStr = lm.getString("historyTypePurchase"); + price = item.getShare().getPurchasePrice(); + } else if (item instanceof Sale sale) { + typeStr = lm.getString("historyTypeSale"); + price = item.getShare().getStock().getSalesPrice(); + + if (sale.getCalculator() instanceof SaleCalculator saleCalc) { + profit = saleCalc.getProfitProperty().getValue(); + } + } else { + typeStr = "ERROR"; + } + + BigDecimal quantity = item.getShare().getQuantity(); + BigDecimal gross = item.getCalculator().calculateGross(); + BigDecimal commission = item.getCalculator().calculateCommission(); + BigDecimal tax = item.getCalculator().calculateTax(); + BigDecimal total = item.getCalculator().calculateTotal(); + + String symbol = item.getShare().getStock().getSymbol(); + + String template = lm.getString("historyCellFormat"); + + setText(String.format(template, + item.getWeek(), + typeStr, + symbol, + FormatBigDecimal.formatNumber(price), + FormatBigDecimal.formatNumber(quantity), + FormatBigDecimal.formatNumber(gross), + FormatBigDecimal.formatNumber(commission), + FormatBigDecimal.formatNumber(tax), + FormatBigDecimal.formatNumber(total), + FormatBigDecimal.formatNumber(profit) + )); + }; + + langSubscription = lm.getBundleProperty() + .subscribe(bundle -> updateTextAction.run()); + + updateTextAction.run(); + + if (item instanceof Purchase) { + setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;"); + } else if (item instanceof Sale) { + setStyle("-fx-text-fill: #27ae60; -fx-font-weight: bold;"); + } + } + } + }); + + HBox searchBox = new HBox(); + searchField = new TextField(); + searchField.promptTextProperty().bind(lm.bindString("search")); + searchBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + searchBox.setPadding(new Insets(8, 14, 8, 14)); + searchBox.setAlignment(Pos.CENTER_LEFT); + searchBox.setMaxWidth(Region.USE_PREF_SIZE); + searchBox.getChildren().addAll(searchField); + + VBox historyListBox = new VBox(historyListView); + historyListBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + + historyListBox.setPadding(new Insets(4)); + historyListBox.setMinHeight(0); + historyListBox.setMaxHeight(Double.MAX_VALUE); + + VBox.setVgrow(historyListView, Priority.ALWAYS); + VBox.setVgrow(historyListBox, Priority.ALWAYS); + + vbox.getChildren().addAll(titleBox, searchBox, historyListBox); + return vbox; + } + + /** + * Sets the player to be observed. + * + *

And puts the {@link TransactionArchive} into a {@link FilteredList} for searching.

+ * + * @param player the player to be observed + */ + public void setPlayer(Player player) { + if (player != null && player.getTransactionArchive().getObservableTransactions() != null) { + + filteredTransactionList = new FilteredList<>( + player.getTransactionArchive().getObservableTransactions()); + + historyListView.setItems(filteredTransactionList); + + searchField.textProperty().addListener(( + observable, oldText, newText) -> { + String search = newText == null ? "" : newText.toLowerCase(); + + filteredTransactionList.setPredicate(transaction -> search.isBlank() + || + transaction.getShare().getStock() + .getSymbol().toLowerCase().contains(search) + || + transaction.getShare().getStock() + .getCompany().toLowerCase().contains(search) + ); + }); + + } else { + historyListView.setItems(null); + } + } +} diff --git a/millions/src/main/resources/Millions-logo.png b/millions/src/main/resources/Millions-logo.png new file mode 100644 index 0000000..328546f Binary files /dev/null and b/millions/src/main/resources/Millions-logo.png differ diff --git a/millions/src/main/resources/Scroll-dot.png b/millions/src/main/resources/Scroll-dot.png new file mode 100644 index 0000000..eaae1d9 Binary files /dev/null and b/millions/src/main/resources/Scroll-dot.png differ diff --git a/millions/src/main/resources/i18n/lang.properties b/millions/src/main/resources/i18n/lang.properties new file mode 100644 index 0000000..87c134e --- /dev/null +++ b/millions/src/main/resources/i18n/lang.properties @@ -0,0 +1,83 @@ +# File for the english language + +# General +yes = Yes +no = No +search = Search: + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History +statusLabelText = Level: +endGameButton = End Game + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week +biggestLoserLabelText = Biggest Loser: +biggestWinnerLabelText = Biggest Winner: + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +portfolioProfit = Profit: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Price: %s | Qty: %s | Gross: %s | Commission: %s | Tax: %s | Total: $%s | Profit: %s + +# EndView +congratsLabel = Thank you for playing Millions!\nBelow you can see what level you reached and the amount of\nmoney you ended up with!\nYou can also choose to start a new game. + +# EndGameAlert +endGameAlertTitle = Ending Game +endGameAlertHeaderText=Warning, this ends the game! +endGameAlertContentText = Are your sure you want to end the game? + + + diff --git a/millions/src/main/resources/i18n/lang_en.properties b/millions/src/main/resources/i18n/lang_en.properties new file mode 100644 index 0000000..1c8ef53 --- /dev/null +++ b/millions/src/main/resources/i18n/lang_en.properties @@ -0,0 +1,80 @@ +# File for the english language + +# General +yes = Yes +no = No +search = Search: + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History +statusLabelText = Level: +endGameButton = End Game + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week +biggestLoserLabelText = Biggest Loser: +biggestWinnerLabelText = Biggest Winner: + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +portfolioProfit = Profit: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Price: %s | Qty: %s | Gross: %s | Commission: %s | Tax: %s | Total: $%s | Profit: %s + +# EndView +congratsLabel = Thank you for playing Millions!\nBelow you can see what level you reachedand the amount of\nmoney you ended up with!\nYou can also choose to start a new game. + +# EndGameAlert +endGameAlertTitle = Ending Game +endGameAlertHeaderText=Warning, this ends the game! +endGameAlertContentText = Are your sure you want to end the game? diff --git a/millions/src/main/resources/i18n/lang_no.properties b/millions/src/main/resources/i18n/lang_no.properties new file mode 100644 index 0000000..b5deee0 --- /dev/null +++ b/millions/src/main/resources/i18n/lang_no.properties @@ -0,0 +1,81 @@ +# File for the norwegian language + +# General +yes = Ja +no = Nei +search = Søk: + +# StartView +newGame = Nytt Spill +languageButtonLabel = Skift Språk +quitGame = Avslutt + +# PlayerNameChooserView +playerNameConfirmButton = Bekreft +playerNameInfo = Vennligst skriv spillernavnet ditt: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Bekreft +playerStartingMoneyInfoLabel = Velg startsaldo for aksjehandel: + +# CSVChooserView +csvHeader = CSV Fil Påkrevd +csvContent = Vennligst velg en .csv fil som inneholder aksjene som skal brukes på børsen i neste vindu. \n\nDersom ingen fil velges (eller .csv filen er tom eller har feil struktur), vil en standard fil bli brukt. \n\nFilen må formates slik: Aksjesymbol,FirmaNavn,PrisTypeDobbel. \n\nEksempel: NVDA,Nvidia,191.27\n\n'#' brukes for kommentarer, og som med feilformaterte linjer, vil slike linjer bli ignorert. +csvFileChooserTitle = Velg CSV fil +csvFileFilterLabel = CSV Filer + +# NavigationBar +navMoneyLabelText = Saldo: +navNetWorthLabelText = Nettoformue: +navPlayerLabelText = Spiller: +marketButton = Marked +portfolioButton = Portefølje +historyButton = Historikk +statusLabelText = Nivå: +endGameButton = Avslutt Spill + +# FooterBar +currentWeekLabelText = Gjeldende Uke: +advanceWeekButton = Neste Uke +biggestLoserLabelText = Største Taper: +biggestWinnerLabelText = Største Vinner: + +# MarketView +marketTitle = Aksjemarked +xAxisLabel = Uke +yAxisLabel = Pris +selectedStock = Valgt aksje: +purchaseQuantity = Antall: +purchaseGross = Brutto: +purchaseCommission = Kurtasje: +purchaseTotal = Total: +purchaseButton = Kjøp +stockCellFormat=%s - %s | Pris: $%s | Prisendring: %s | Høyeste pris: %s | Laveste pris: %s + +# PortfolioView +portfolioTitle = Portefølje +selectedShare = Valgt andel: +portfolioGross = Brutto: +portfolioCommission = Kurtasje: +portfolioTax = Skatt: +portfolioTotal = Total: +portfolioProfit = Profitt: +sellButton = Selg +portfolioShareQuantity = Ant: +portfolioBuyPrice = Kjøpspris: +portfolioCurrentPrice = Gjeldende pris: + +# TransactionArchiveView +historyTitle=Transaksjonshistorikk +historyTypePurchase=Kjøp +historyTypeSale=Salg +historyCellFormat=Uke %d | %s | %s | Pris: %s | Ant: %s | Brutto: %s | Kurtasje: %s | Skatt: %s | Total: $%s | Profit: %s + + +# EndView +congratsLabel = Takk for at du spilte Millions!\nNedenfor kan du se hvilket nivå du nådde opp til\nog din endelige saldo!\nDu kan også velge å starte et nytt spill. + +# EndGameAlert +endGameAlertTitle = Avslutter Spill +endGameAlertHeaderText= Advarsel, dette vil avslutte spillet! +endGameAlertContentText = Er du sikker på at du vil avslutte spillet? \ No newline at end of file diff --git a/millions/src/main/resources/sp500.csv b/millions/src/main/resources/sp500.csv new file mode 100644 index 0000000..d9cec61 --- /dev/null +++ b/millions/src/main/resources/sp500.csv @@ -0,0 +1,506 @@ +# S&P 500 Companies by Market Cap +# Ticker,Name,Price + +NVDA,Nvidia,191.27 +AAPL,Apple Inc.,276.43 +MSFT,Microsoft,404.68 +AMZN,Amazon,204.62 +GOOGL,Alphabet Inc. (Class A),311.20 +GOOG,Alphabet Inc. (Class C),311.62 +META,Meta Platforms,669.41 +AVGO,Broadcom,343.35 +TSLA,Tesla Inc.,426.52 +BRK.B,Berkshire Hathaway,501.05 +WMT,Walmart,128.75 +LLY,Lilly (Eli),1014.43 +JPM,JPMorgan Chase,311.14 +XOM,ExxonMobil,155.28 +V,Visa Inc.,329.54 +JNJ,Johnson & Johnson,240.70 +MA,Mastercard,539.52 +MU,Micron Technology,411.25 +ORCL,Oracle Corporation,157.08 +COST,Costco,979.71 +BAC,Bank of America,54.06 +ABBV,AbbVie,220.17 +HD,Home Depot (The),389.46 +PG,Procter & Gamble,159.45 +CVX,Chevron Corporation,185.66 +CAT,Caterpillar Inc.,773.53 +AMD,Advanced Micro Devices,213.00 +CSCO,Cisco,85.82 +KO,Coca-Cola Company (The),78.51 +NFLX,Netflix,79.94 +GE,GE Aerospace,314.37 +PLTR,Palantir Technologies,135.59 +LRCX,Lam Research,236.60 +MRK,Merck & Co.,118.79 +PM,Philip Morris International,185.99 +GS,Goldman Sachs,949.29 +MS,Morgan Stanley,176.86 +WFC,Wells Fargo,89.07 +AMAT,Applied Materials,342.19 +RTX,RTX Corporation,197.56 +IBM,IBM,273.86 +UNH,UnitedHealth Group,278.79 +AXP,American Express,355.18 +INTC,Intel,47.85 +TMUS,T-Mobile US,207.62 +PEP,PepsiCo,168.71 +MCD,McDonald's,323.50 +GEV,GE Vernova,822.50 +LIN,Linde plc,466.04 +C,Citigroup,117.93 +TXN,Texas Instruments,226.34 +VZ,Verizon,48.55 +T,AT&T,28.21 +TMO,Thermo Fisher Scientific,525.00 +AMGN,Amgen,364.77 +ABT,Abbott Laboratories,112.97 +KLAC,KLA Corporation,1492.27 +GILD,Gilead Sciences,155.71 +DIS,Walt Disney Company (The),108.30 +NEE,NextEra Energy,91.19 +BA,Boeing,236.98 +ANET,Arista Networks,141.06 +APH,Amphenol,144.60 +ISRG,Intuitive Surgical,496.14 +CRM,Salesforce,184.94 +SCHW,Charles Schwab Corporation,95.50 +BLK,BlackRock,1083.35 +TJX,TJX Companies,150.56 +DE,Deere & Company,610.03 +ADI,Analog Devices,336.71 +LOW,Lowe's,286.57 +PFE,Pfizer,27.77 +UNP,Union Pacific Corporation,261.95 +DHR,Danaher Corporation,219.80 +APP,AppLovin Corporation,459.27 +HON,Honeywell,243.56 +ETN,Eaton Corporation,394.94 +QCOM,Qualcomm,141.90 +UBER,Uber,70.72 +LMT,Lockheed Martin,630.54 +WELL,Welltower,208.65 +ACN,Accenture,230.79 +BKNG,Booking Holdings,4322.85 +SYK,Stryker Corporation,362.53 +COP,ConocoPhillips,110.75 +NEM,Newmont,123.89 +COF,Capital One,214.93 +PLD,Prologis,140.35 +MDT,Medtronic,100.87 +CB,Chubb Limited,328.82 +PH,Parker Hannifin,998.24 +PGR,Progressive Corporation,209.33 +BMY,Bristol Myers Squibb,60.18 +HCA,HCA Healthcare,531.83 +SPGI,S&P Global,396.65 +CMCSA,Comcast,32.53 +VRTX,Vertex Pharmaceuticals,459.93 +MCK,McKesson Corporation,944.20 +PANW,Palo Alto Networks,165.49 +GLW,Corning Inc.,134.16 +SBUX,Starbucks,99.03 +INTU,Intuit,401.05 +MO,Altria,65.72 +BSX,Boston Scientific,73.76 +CME,CME Group,303.44 +NOW,ServiceNow,101.55 +ADBE,Adobe Inc.,258.39 +TT,Trane Technologies,473.75 +CRWD,CrowdStrike,414.94 +BX,Blackstone Inc.,133.29 +UPS,United Parcel Service,119.93 +SO,Southern Company,90.86 +CEG,Constellation Energy,274.37 +DUK,Duke Energy,124.86 +CVS,CVS Health,76.36 +MAR,Marriott International,360.71 +NOC,Northrop Grumman,680.45 +PNC,PNC Financial Services,237.28 +WM,Waste Management,234.67 +GD,General Dynamics,348.79 +WDC,Western Digital,277.26 +KKR,KKR,105.28 +HWM,Howmet Aerospace,233.13 +FCX,Freeport-McMoRan,65.30 +NKE,Nike Inc.,62.43 +USB,U.S. Bancorp,59.25 +MMM,3M,173.46 +SHW,Sherwin-Williams,364.16 +RCL,Royal Caribbean Group,331.76 +SNDK,Sandisk Corporation,607.86 +STX,Seagate Technology,409.41 +EMR,Emerson Electric,156.30 +ADP,Automatic Data Processing,217.59 +WMB,Williams Companies,71.48 +ICE,Intercontinental Exchange,153.60 +FDX,FedEx,368.49 +ITW,Illinois Tool Works,298.50 +JCI,Johnson Controls,140.75 +CRH,CRH plc,127.89 +ECL,Ecolab,301.35 +EQIX,Equinix,863.66 +BK,BNY Mellon,122.83 +MRSH,Marsh & McLennan Companies Inc.,174.09 +AMT,American Tower,179.46 +CMI,Cummins,601.45 +SNPS,Synopsys,433.56 +REGN,Regeneron Pharmaceuticals,780.09 +DELL,Dell Technologies,124.37 +CDNS,Cadence Design Systems,298.74 +CTAS,Cintas,201.10 +ORLY,O'Reilly Auto Parts,93.87 +MNST,Monster Beverage,80.88 +MDLZ,Mondelez International,61.45 +PWR,Quanta Services,523.69 +CI,Cigna,292.46 +CSX,CSX Corporation,41.30 +CL,Colgate-Palmolive,95.04 +SLB,Schlumberger,51.14 +HLT,Hilton Worldwide,327.38 +DASH,DoorDash,175.41 +TDG,TransDigm Group,1325.26 +MCO,Moody's Corporation,415.20 +APO,Apollo Global Management,127.53 +ELV,Elevance Health,329.59 +ABNB,Airbnb,119.56 +GM,General Motors,79.78 +NSC,Norfolk Southern Railway,316.73 +COR,Cencora,365.43 +MSI,Motorola Solutions,423.10 +KMI,Kinder Morgan,31.64 +RSG,Republic Services,226.72 +HOOD,Robinhood Markets Inc.,77.55 +WBD,Warner Bros. Discovery,28.01 +TFC,Truist Financial,54.42 +PCAR,Paccar,129.93 +AON,Aon,314.02 +TEL,TE Connectivity,227.16 +APD,Air Products,293.38 +AEP,American Electric Power,122.18 +FTNT,Fortinet,87.72 +TRV,Travelers Companies (The),299.75 +PSX,Phillips 66,161.13 +LHX,L3Harris,341.14 +EOG,EOG Resources,117.39 +SPG,Simon Property Group,195.66 +NXPI,NXP Semiconductors,249.26 +ROST,Ross Stores,192.31 +VLO,Valero Energy,203.89 +AZO,AutoZone,3733.09 +MPC,Marathon Petroleum,207.85 +BKR,Baker Hughes,61.16 +AFL,Aflac,116.20 +DLR,Digital Realty,174.16 +SRE,Sempra,90.83 +O,Realty Income,64.39 +MPWR,Monolithic Power Systems,1197.55 +GWW,W. W. Grainger,1202.13 +ZTS,Zoetis,128.19 +CARR,Carrier Global,66.80 +D,Dominion Energy,64.61 +F,Ford Motor Company,13.78 +URI,United Rentals,870.17 +AME,Ametek,236.33 +VST,Vistra Corp.,160.43 +FAST,Fastenal,47.14 +ALL,Allstate,205.91 +OKE,ONEOK,85.03 +AJG,Arthur J. Gallagher & Co.,207.61 +CAH,Cardinal Health,225.15 +CVNA,Carvana Co.,365.94 +IDXX,Idexx Laboratories,647.63 +MET,MetLife,78.87 +TGT,Target Corporation,114.12 +PSA,Public Storage,293.33 +BDX,Becton Dickinson,179.62 +CTVA,Corteva,75.48 +TER,Teradyne,323.92 +EA,Electronic Arts,201.72 +ADSK,Autodesk,232.93 +FITB,Fifth Third Bancorp,54.59 +CMG,Chipotle Mexican Grill,37.35 +FANG,Diamondback Energy,168.93 +TRGP,Targa Resources,222.03 +FIX,Comfort Systems USA Inc.,1345.62 +DHI,D. R. Horton,163.35 +HSY,Hershey Company (The),231.46 +OXY,Occidental Petroleum,47.34 +DAL,Delta Air Lines,71.16 +ROK,Rockwell Automation,413.43 +NDAQ,Nasdaq Inc.,81.05 +XEL,Xcel Energy,77.79 +EW,Edwards Lifesciences,78.67 +CCL,Carnival,32.80 +CBRE,CBRE Group,151.76 +ETR,Entergy,100.79 +EXC,Exelon,44.57 +AMP,Ameriprise Financial,489.27 +NUE,Nucor,194.78 +DDOG,Datadog,126.70 +YUM,Yum! Brands,160.35 +MCHP,Microchip Technology,80.90 +WAB,Wabtec,255.15 +KR,Kroger,68.68 +AIG,American International Group,79.36 +VMC,Vulcan Materials Company,321.01 +CIEN,Ciena Corporation,301.11 +SYY,Sysco,88.04 +PEG,Public Service Enterprise Group,83.85 +COIN,Coinbase Global,152.61 +ODFL,Old Dominion,195.74 +KEYS,Keysight Technologies,237.88 +KDP,Keurig Dr Pepper,29.84 +VTR,Ventas,85.29 +MLM,Martin Marietta Materials,663.38 +GRMN,Garmin,206.52 +ED,Consolidated Edison,109.36 +HIG,Hartford (The),142.02 +LVS,Las Vegas Sands,57.75 +CPRT,Copart,39.73 +EL,Estée Lauder Companies (The),106.09 +IR,Ingersoll Rand,96.98 +WDAY,Workday Inc.,145.19 +MSCI,MSCI,519.16 +TTWO,Take-Two Interactive,204.25 +RMD,ResMed,259.29 +EBAY,eBay,82.95 +PCG,PG&E Corporation,17.02 +CCI,Crown Castle,85.66 +PYPL,PayPal,40.26 +PRU,Prudential Financial,105.34 +WEC,WEC Energy Group,113.19 +UAL,United Airlines Holdings,113.50 +STT,State Street Corporation,131.37 +HBAN,Huntington Bancshares,18.02 +A,Agilent Technologies,128.22 +GEHC,GE HealthCare,79.29 +MTB,M&T Bank,235.06 +EME,EMCOR Group Inc.,803.53 +ACGL,Arch Capital Group,98.49 +KMB,Kimberly-Clark,107.35 +ROP,Roper Technologies,333.96 +EQT,EQT Corporation,56.73 +KVUE,Kenvue,18.44 +LYV,Live Nation Entertainment,150.60 +OTIS,Otis Worldwide,89.85 +AXON,Axon Enterprise,436.36 +NRG,NRG Energy,160.11 +CTSH,Cognizant,71.22 +IBKR,Interactive Brokers Group,76.54 +PAYX,Paychex,94.49 +FISV,Fiserv Inc.,62.72 +ADM,Archer Daniels Midland,69.24 +XYZ,Block Inc.,53.87 +FICO,Fair Isaac,1369.86 +DG,Dollar General,147.49 +DOV,Dover Corporation,232.52 +ROL,Rollins Inc.,65.74 +HPE,Hewlett Packard Enterprise,23.62 +RJF,Raymond James Financial,159.60 +TPR,Tapestry Inc.,154.43 +VICI,Vici Properties,29.18 +TDY,Teledyne Technologies,657.92 +XYL,Xylem Inc.,126.71 +CHTR,Charter Communications,241.08 +ARES,Ares Management Corporation,137.90 +ULTA,Ulta Beauty,684.25 +STLD,Steel Dynamics,206.43 +EXR,Extra Space Storage,142.02 +LEN,Lennar,120.76 +IQV,IQVIA,175.80 +IRM,Iron Mountain,99.67 +KHC,Kraft Heinz,24.88 +PPG,PPG Industries,130.53 +HAL,Halliburton,34.84 +ATO,Atmos Energy,175.22 +DTE,DTE Energy,139.17 +TSCO,Tractor Supply,54.53 +EXPE,Expedia Group,235.08 +CFG,Citizens Financial Group,66.87 +AEE,Ameren,106.08 +TPL,Texas Pacific Land Corporation,416.14 +CBOE,Cboe Global Markets,271.99 +ON,ON Semiconductor,70.68 +MTD,Mettler Toledo,1391.33 +STZ,Constellation Brands,163.02 +BIIB,Biogen,191.29 +DVN,Devon Energy,44.78 +FE,FirstEnergy,47.94 +JBL,Jabil,260.92 +NTRS,Northern Trust,147.45 +HUBB,Hubbell Incorporated,513.63 +WTW,Willis Towers Watson,282.86 +WRB,W. R. Berkley Corporation,71.20 +RF,Regions Financial Corporation,30.86 +PHM,PulteGroup,139.04 +CNP,CenterPoint Energy,40.91 +PPL,PPL Corporation,35.97 +DXCM,Dexcom,68.19 +SW,Smurfit WestRock,50.23 +ES,Eversource Energy,69.77 +GIS,General Mills,48.40 +EIX,Edison International,66.94 +IP,International Paper,48.64 +WSM,Williams-Sonoma,214.52 +CINF,Cincinnati Financial,164.11 +LUV,Southwest Airlines,51.66 +AVB,AvalonBay Communities,179.80 +SYF,Synchrony Financial,72.95 +FIS,Fidelity National Information Services,48.73 +KEY,KeyCorp,22.67 +DLTR,Dollar Tree,125.35 +EQR,Equity Residential,65.09 +EXE,Expand Energy,103.09 +DRI,Darden Restaurants,212.58 +FSLR,First Solar,227.60 +DOW,Dow Inc.,33.98 +CPAY,Corpay,346.25 +AWK,American Water Works,123.54 +CHD,Church & Dwight,100.17 +LH,LabCorp,289.08 +VRSK,Verisk Analytics,171.87 +Q,Qnity Electronics,114.06 +CTRA,Coterra,31.48 +STE,Steris,243.09 +EFX,Equifax,197.29 +VLTO,Veralto,95.33 +BG,Bunge Global,121.39 +DGX,Quest Diagnostics,209.53 +CHRW,C.H. Robinson,196.77 +AMCR,Amcor,49.62 +TSN,Tyson Foods,64.52 +L,Loews Corporation,110.15 +CMS,CMS Energy,74.16 +BRO,Brown & Brown,67.07 +LDOS,Leidos,174.83 +PKG,Packaging Corporation of America,244.06 +JBHT,J.B. Hunt,231.62 +OMC,Omnicom Group,69.53 +EXPD,Expeditors International,163.06 +RL,Ralph Lauren Corporation,359.55 +NVR,NVR Inc.,8082.38 +DD,DuPont,51.29 +HUM,Humana,176.34 +NI,NiSource,44.78 +NTAP,NetApp,105.69 +GPC,Genuine Parts Company,149.43 +LULU,Lululemon Athletica,176.88 +ALB,Albemarle Corporation,176.03 +TROW,T. Rowe Price,94.24 +PFG,Principal Financial Group,92.86 +CSGP,CoStar Group,48.09 +GPN,Global Payments,72.67 +SBAC,SBA Communications,190.43 +SNA,Snap-on,383.21 +CNC,Centene Corporation,40.26 +VRSN,Verisign,215.66 +WAT,Waters Corporation,331.52 +IFF,International Flavors & Fragrances,76.65 +BR,Broadridge Financial Solutions,167.88 +WY,Weyerhaeuser,27.09 +INCY,Incyte,99.35 +LII,Lennox International,553.57 +LYB,LyondellBasell,59.29 +SMCI,Supermicro,31.69 +MKC,McCormick & Company,70.30 +ZBH,Zimmer Biomet,95.21 +PTC,PTC Inc.,155.52 +FTV,Fortive,58.93 +VTRS,Viatris,16.02 +EVRG,Evergy,78.94 +BALL,Ball Corporation,67.29 +HPQ,HP Inc.,19.63 +WST,West Pharmaceutical Services,247.87 +PODD,Insulet Corporation,253.09 +APTV,Aptiv,83.64 +CDW,CDW,135.32 +LNT,Alliant Energy,68.17 +TXT,Textron,96.68 +ESS,Essex Property Trust,262.76 +HOLX,Hologic,75.11 +J,Jacobs Solutions,142.70 +INVH,Invitation Homes,27.16 +TKO,TKO Group Holdings,210.88 +NDSN,Nordson Corporation,295.64 +DECK,Deckers Brands,115.15 +PNR,Pentair,99.79 +COO,Cooper Companies (The),82.84 +MAA,Mid-America Apartment Communities,136.74 +FFIV,F5 Inc.,282.22 +MAS,Masco,76.24 +IEX,IDEX Corporation,211.28 +MRNA,Moderna,40.21 +TRMB,Trimble Inc.,65.06 +ALLE,Allegion,179.40 +HII,Huntington Ingalls Industries,392.15 +CLX,Clorox,125.65 +CF,CF Industries,97.23 +GEN,Gen Digital,24.64 +AVY,Avery Dennison,192.77 +KIM,Kimco Realty,21.96 +HAS,Hasbro,105.26 +ERIE,Erie Indemnity,279.84 +TYL,Tyler Technologies,339.51 +UHS,Universal Health Services,231.22 +BEN,Franklin Resources,27.64 +ALGN,Align Technology,197.24 +SOLV,Solventum,81.25 +BBY,Best Buy,66.84 +REG,Regency Centers,76.49 +SWK,Stanley Black & Decker,90.31 +BF.B,Brown–Forman,30.11 +BLDR,Builders FirstSource,125.44 +HST,Host Hotels & Resorts,19.98 +AKAM,Akamai Technologies,95.00 +EG,Everest Group,331.67 +UDR,UDR Inc.,39.84 +TTD,The Trade Desk Inc.,27.19 +HRL,Hormel Foods,23.79 +DPZ,Domino's,385.50 +ZBRA,Zebra Technologies,250.71 +GNRC,Generac,214.94 +FOX,Fox Corporation (Class B),56.02 +FOXA,Fox Corporation (Class A),61.76 +GDDY,GoDaddy,91.44 +PSKY,Paramount Skydance Corp,10.96 +WYNN,Wynn Resorts,115.18 +JKHY,Jack Henry & Associates,165.63 +CPT,Camden Property Trust,111.66 +DOC,Healthpeak Properties,16.95 +SJM,J.M. Smucker Company (The),109.98 +IVZ,Invesco,26.42 +AES,AES Corporation,16.45 +IT,Gartner,160.21 +GL,Globe Life,144.35 +BAX,Baxter International,22.30 +PNW,Pinnacle West,95.57 +RVTY,Revvity,100.89 +AOS,A. O. Smith,80.03 +AIZ,Assurant,216.77 +TAP,Molson Coors Beverage Company,52.98 +NCLH,Norwegian Cruise Line Holdings,22.76 +POOL,Pool Corporation,270.70 +EPAM,EPAM Systems,180.79 +APA,APA Corporation,28.11 +TECH,Bio-Techne,63.46 +MOS,Mosaic Company (The),31.21 +BXP,BXP Inc.,61.61 +DVA,DaVita,144.55 +SWKS,Skyworks Solutions,63.62 +HSIC,Henry Schein,81.21 +CAG,Conagra Brands,19.95 +MGM,MGM Resorts,36.39 +ARE,Alexandria Real Estate Equities,53.92 +FRT,Federal Realty Investment Trust,107.14 +CPB,Campbell Soup Company,29.16 +NWSA,News Corp (Class A),23.30 +CRL,Charles River Laboratories,164.83 +MTCH,Match Group,31.27 +FDS,FactSet,193.29 +LW,Lamb Weston,50.33 +PAYC,Paycom,117.68 +MOH,Molina Healthcare,122.46 +NWS,News Corp (Class B),26.91 diff --git a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java deleted file mode 100644 index d01ca79..0000000 --- a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java +++ /dev/null @@ -1,165 +0,0 @@ -package no.ntnu.gruppe53; - - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; - -@DisplayName("Exhange Test") -public class ExchangeTest { - - private Stock appleStock; - - private Stock microStock; - - private Stock ntnuStock; - - private ArrayList stocks; - - - private Share appleShare; - private Share ntnuShare; - - private Exchange exchange; - - - - private Player testPlayer; - - @BeforeEach - void setUp() { - appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); - microStock = new Stock("MSFT", "Microsoft", new BigDecimal("50")); - ntnuStock = new Stock("NTNU", "Ntnu", new BigDecimal("1000")); - - appleShare = new Share(appleStock, new BigDecimal("10"), new BigDecimal("40")); - ntnuShare = new Share(ntnuStock, new BigDecimal("10"), new BigDecimal("40")); - - - testPlayer = new Player("Bob", new BigDecimal("2000")); - - testPlayer.getPortfolio().addShare(appleShare); - testPlayer.getPortfolio().addShare(ntnuShare); - - - - - stocks = new ArrayList<>(); - stocks.add(appleStock); - stocks.add(microStock); - stocks.add(ntnuStock); - - exchange = new Exchange("Nasdaq", stocks); - - - - - - - } - - @Test - @DisplayName("Has Stock test") - void hasStockTest() { - - Boolean expected = Boolean.TRUE; - Boolean result = exchange.hasStock("AAPL"); - - assertEquals(expected, result); - - - } - - @Test - @DisplayName("Get Stock Test") - void getStockTest() { - - Stock expected = appleStock; - Stock result = exchange.getStock("AAPL"); - - assertEquals(expected, result); - - } - - @Test - @DisplayName("Find Stock test") - void findStockTest() { - - } - - @Test - @DisplayName("Buy test") - void buyTest() { - - - Share testMSFTShare = new Share(microStock, new BigDecimal("5"), microStock.getSalesPrice()); - - exchange.buy("MSFT", new BigDecimal("5"), testPlayer); - - BigDecimal appleExpected = new BigDecimal("10"); - - BigDecimal ntnuExpected = new BigDecimal("10"); - BigDecimal microExpected = new BigDecimal("5"); - - - - - - - assertEquals(appleExpected, testPlayer.getPortfolio().getShares().get(0).getQuantity()); - assertEquals(ntnuExpected, testPlayer.getPortfolio().getShares().get(1).getQuantity()); - assertEquals(microExpected, testPlayer.getPortfolio().getShares().get(2).getQuantity()); - } - - @Test - @DisplayName("Sell test") - void sellTest() { - - exchange.sell(appleShare, testPlayer); - - BigDecimal ntnuExpected = new BigDecimal("10"); - - - assertEquals(ntnuExpected, testPlayer.getPortfolio().getShares().get(0).getQuantity()); - - } - - @Test - @DisplayName("Advance a week test") - void advanceWeekTest() { - - int currentWeek = exchange.getWeek(); - - BigDecimal applePrice = exchange.getStock(appleStock.getSymbol()).getSalesPrice(); - BigDecimal microPrice = exchange.getStock(microStock.getSymbol()).getSalesPrice(); - BigDecimal ntnuPrice = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice(); - - exchange.advance(); - - BigDecimal applePriceChange = exchange.getStock(appleStock.getSymbol()).getSalesPrice(); - BigDecimal microPriceChange = exchange.getStock(microStock.getSymbol()).getSalesPrice(); - BigDecimal ntnuPriceChange = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice(); - - - - - - assertEquals(currentWeek + 1, exchange.getWeek()); - - assertNotEquals(applePrice, applePriceChange); - assertNotEquals(microPrice, microPriceChange); - assertNotEquals(ntnuPrice, ntnuPriceChange); - - System.out.println("Apple stock changed from: " + applePrice + " to " + applePriceChange); - System.out.println("Microsoft stock changed from: " + microPrice + " to " + microPriceChange); - System.out.println("Ntnu stock changed from: " + ntnuPrice + " to " + ntnuPriceChange); - - } - -} diff --git a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java deleted file mode 100644 index 76c4914..0000000 --- a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package no.ntnu.gruppe53; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -import static no.ntnu.gruppe53.FileHandler.*; -import static org.junit.jupiter.api.Assertions.*; - -class FileHandlerTest { - @Test - void writeTest() throws IOException { - String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\"; - String fileName = "stockTest.csv"; - - - String symbol1 = "AAPL"; - String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); - - String symbol2 = "NVDA"; - String company2 = "Nvidia"; - BigDecimal price2 = new BigDecimal("191.27"); - - Stock testStock1 = new Stock(symbol1,company1, price1); - Stock testStock2 = new Stock(symbol2, company2, price2); - - List stockList = new ArrayList<>(); - stockList.add(testStock1); - stockList.add(testStock2); - - writeStocksToFile(stockList, filePathString, fileName); - - List lines = Files.readAllLines(Paths.get(filePathString + fileName)); - - assertTrue(Files.exists(Paths.get(filePathString + fileName)), - "File should have been created successfully."); - assertEquals("#Ticker,Name,Price",lines.getFirst(), "First line " + - "should contain correct headers."); - assertEquals("", lines.get(1), "Second line should be empty."); - assertEquals(symbol1 + "," + company1 + "," + price1, lines.get(2), - "Third line should contain correct values for first stock."); - assertEquals(symbol2 + "," + company2 + "," + price2, lines.getLast(), - "Last line should contain correct values for second stock."); - } - - @Test - void readTest() { - String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\"; - String fileName = "stockTest.csv"; - - - String symbol1 = "AAPL"; - String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); - - String symbol2 = "NVDA"; - String company2 = "Nvidia"; - BigDecimal price2 = new BigDecimal("191.27"); - - Stock testStock1 = new Stock(symbol1,company1, price1); - Stock testStock2 = new Stock(symbol2, company2, price2); - - List stockList = new ArrayList<>(); - stockList.add(testStock1); - stockList.add(testStock2); - - writeStocksToFile(stockList, filePathString, fileName); - - List readStocks = readStocksFromFile(filePathString, fileName); - - assertEquals(symbol1, readStocks.getFirst().getSymbol(), - "First stock's symbol should be AAPL."); - assertEquals(company1, readStocks.getFirst().getCompany(), - "First stock's company name should be Apple Inc."); - assertEquals(0, price1.compareTo(readStocks.getFirst().getSalesPrice()), - "First stock's price should be 100."); - assertEquals(symbol2, readStocks.getLast().getSymbol(), - "Second stock's symbol should be NVDA."); - assertEquals(company2, readStocks.getLast().getCompany(), - "Second stock's company name should be Nvidia."); - assertEquals(0, price2.compareTo(readStocks.getLast().getSalesPrice()), - "Second stock's price should be 191.27"); - } -} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java b/millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java deleted file mode 100644 index 6434eff..0000000 --- a/millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package no.ntnu.gruppe53; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -//The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works - -@DisplayName("Sale Calculator Test") -public class SaleCalculatorUnitTest { - - private Stock testStock = new Stock("AAPL", "Apple", new BigDecimal(100)); - - private Share testShare = new Share(testStock, new BigDecimal(10), new BigDecimal(40)); - - - private SaleCalculator saleCalculator = new SaleCalculator(testShare); - - @Test - @DisplayName("Sale Calculator: Calculate Gross") - void testCalculateGross() { - - BigDecimal expected = new BigDecimal(1000).setScale(2); - BigDecimal result = saleCalculator.calculateGross().setScale(2); - - assertEquals(expected, result); - - } - - @Test - @DisplayName("Commision of the sale transaction") - void testCalculateCommission() { - - BigDecimal expected = new BigDecimal(10).setScale(2); - BigDecimal result = saleCalculator.calculateCommission().setScale(2); - - assertEquals(expected, result); - - } - - @Test - @DisplayName("Tax of the sale transaction") - void testCalculateTax() { - - BigDecimal expected = new BigDecimal(177).setScale(2); - BigDecimal result = saleCalculator.calculateTax().setScale(2); - - assertEquals(expected, result); - - } - - @Test - @DisplayName("Total value of sale") - void testCalculateTotal() { - - BigDecimal expected = new BigDecimal(813).setScale(2); - BigDecimal result = saleCalculator.calculateTotal().setScale(2); - - assertEquals(expected, result); - - } - -} diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java new file mode 100644 index 0000000..6e28192 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java @@ -0,0 +1,357 @@ +package no.ntnu.gruppe53.model; + + +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Exchange Test") +public class ExchangeTest { + + private Stock appleStock; + + private Stock microStock; + + private Stock ntnuStock; + + private ArrayList stocks; + + + private Share appleShare; + private Share ntnuShare; + + private Exchange exchange; + + + + private Player testPlayer; + + @BeforeEach + void setUp() { + appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); + microStock = new Stock("MSFT", "Microsoft", new BigDecimal("50")); + ntnuStock = new Stock("NTNU", "Ntnu", new BigDecimal("1000")); + + appleShare = new Share(appleStock, new BigDecimal("10"), new BigDecimal("40")); + ntnuShare = new Share(ntnuStock, new BigDecimal("10"), new BigDecimal("40")); + + + testPlayer = new Player("Bob", new BigDecimal("2000")); + + testPlayer.getPortfolio().addShare(appleShare); + testPlayer.getPortfolio().addShare(ntnuShare); + + + + + stocks = new ArrayList<>(); + stocks.add(appleStock); + stocks.add(microStock); + stocks.add(ntnuStock); + + exchange = new Exchange("Nasdaq", stocks); + + + + + + + } + + @Test + @DisplayName("Has Stock test") + void hasStockTest() { + + Boolean expected = Boolean.TRUE; + Boolean result = exchange.hasStock("AAPL"); + + assertEquals(expected, result); + + + } + + @Test + @DisplayName("Get Stock Test") + void getStockTest() { + + Stock expected = appleStock; + Stock result = exchange.getStock("AAPL"); + + assertEquals(expected, result); + + } + + @Test + @DisplayName("Find Stock test") + void findStockTest() { + + } + + @Test + @DisplayName("Buy test") + void buyTest() { + + + Share testMSFTShare = new Share(microStock, new BigDecimal("5"), microStock.getSalesPrice()); + + exchange.buy("MSFT", new BigDecimal("5"), testPlayer); + + BigDecimal appleExpected = new BigDecimal("10"); + + BigDecimal ntnuExpected = new BigDecimal("10"); + BigDecimal microExpected = new BigDecimal("5"); + + + + + + + assertEquals(appleExpected, testPlayer.getPortfolio().getShares().get(0).getQuantity()); + assertEquals(ntnuExpected, testPlayer.getPortfolio().getShares().get(1).getQuantity()); + assertEquals(microExpected, testPlayer.getPortfolio().getShares().get(2).getQuantity()); + } + + @Test + @DisplayName("Sell test") + void sellTest() { + + exchange.sell(appleShare, testPlayer); + + BigDecimal ntnuExpected = new BigDecimal("10"); + + + assertEquals(ntnuExpected, testPlayer.getPortfolio().getShares().get(0).getQuantity()); + + } + + @Test + @DisplayName("Advance a week test") + void advanceWeekTest() { + + int currentWeek = exchange.getWeek(); + + BigDecimal applePrice = exchange.getStock(appleStock.getSymbol()).getSalesPrice(); + BigDecimal microPrice = exchange.getStock(microStock.getSymbol()).getSalesPrice(); + BigDecimal ntnuPrice = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice(); + + exchange.advance(); + + BigDecimal applePriceChange = exchange.getStock(appleStock.getSymbol()).getSalesPrice(); + BigDecimal microPriceChange = exchange.getStock(microStock.getSymbol()).getSalesPrice(); + BigDecimal ntnuPriceChange = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice(); + + + + + + assertEquals(currentWeek + 1, exchange.getWeek()); + + assertNotEquals(applePrice, applePriceChange); + assertNotEquals(microPrice, microPriceChange); + assertNotEquals(ntnuPrice, ntnuPriceChange); + + System.out.println("Apple stock changed from: " + applePrice + " to " + applePriceChange); + System.out.println("Microsoft stock changed from: " + microPrice + " to " + microPriceChange); + System.out.println("Ntnu stock changed from: " + ntnuPrice + " to " + ntnuPriceChange); + + } + + @Test + void getGainersShouldSortCorrectly() { + appleStock.addNewSalesPrice(new BigDecimal("3000")); + microStock.addNewSalesPrice(new BigDecimal("100")); + ntnuStock.addNewSalesPrice(new BigDecimal("1500")); + + var gainList = exchange.getGainers(3); + + assertEquals(appleStock.getSymbol(), gainList.getFirst().getSymbol(), + "Biggest gainer should be 'AAPL'."); + assertEquals(ntnuStock.getSymbol(), gainList.get(1).getSymbol(), + "Second biggest gainer should be 'NTNU'."); + assertEquals(microStock.getSymbol(), gainList.getLast().getSymbol(), + "Third biggest gainer should be 'MSFT'."); + } + + @Test + void getLosersShouldSortCorrectly() { + appleStock.addNewSalesPrice(new BigDecimal("3000")); + microStock.addNewSalesPrice(new BigDecimal("100")); + ntnuStock.addNewSalesPrice(new BigDecimal("1500")); + + var gainList = exchange.getLosers(3); + + System.out.println(gainList.size()); + + assertEquals(microStock.getSymbol(), gainList.get(0).getSymbol(), + "Biggest loser should me 'MSFT'"); + assertEquals(ntnuStock.getSymbol(), gainList.get(1).getSymbol(), + "Second biggest loser should be 'NTNU'"); + assertEquals(appleStock.getSymbol(), gainList.get(2).getSymbol(), + "Least biggest loser should be 'AAPL'"); + } + + @Test + void getTopStocksShouldThrowIllegalArgumentExceptionsIfEntriesIsNegative() { + assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1), + "It should not be possible to list a negative amount of entries."); + assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-2), + "It should not be possible to list a negative amount of entries."); + + } + + @Test + void nullExchangeNameShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new Exchange(null, stocks), + "Exchange name should not be allowed to be null."); + } + + @Test + void blankExchangeNameShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new Exchange(" ", stocks), + "Exchange name should not be allowed to be blank."); + } + + @Test + void nullStockListShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", null), + "Stocklist should not be allowed to be null."); + } + + @Test + void nullStockInListShouldThrowIllegalArgumentException() { + stocks.add(null); + + assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", stocks), + "Exchange should not be allowed to contain a null value for a stock."); + } + + @Test + void duplicateStockSymbolInExchangeShouldThrowIllegalArgumentException() { + stocks.add(new Stock("AAPL", "Eple", new BigDecimal("50"))); + + assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", stocks), + "Duplicate stock symbol should not be allowed"); + } + + @Test + void getNameShouldReturnExchangeName() { + assertEquals("Nasdaq", exchange.getName(), + "Exchange name should be correct."); + } + + @Test + void getStockShouldReturnStock() { + assertEquals(microStock, exchange.getStock("MSFT"), + "Should return the correct stock based on the ticker symbol."); + } + + @Test + void getStockForNullStockShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.getStock(null), + "Searching using null stock should throw an exception."); + } + + @Test + void getStockForStockNotInExchangeShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.getStock("AAAA"), + "Searching for stock not in exchange should throw an exception."); + } + + @Test + void getObservableListShouldReturnAnObservableList() { + assertInstanceOf(ObservableList.class, exchange.getObservableStocks(), + "Returned object should be an observableList."); + } + + @Test + void findStocksForNullStringShouldGiveEmptyList() { + List listStocks = exchange.findStocks(null); + + assertTrue(listStocks.isEmpty(), "List should be empty."); + } + + @Test + void findStocksForBlankStringShouldGiveEmptyList() { + List listStocks = exchange.findStocks(" "); + + assertTrue(listStocks.isEmpty(), "List should be empty."); + } + + @Test + void findStocksShouldReturnRelevantStock() { + List listStocks = exchange.findStocks("MSFT"); + + assertEquals(listStocks.getFirst(), microStock, "Should return the correct stock."); + } + + @Test + void findStocksShouldReturnStocksThatContainString() { + List listStocks = exchange.findStocks("T"); + + assertEquals(2, listStocks.size(), "Should return correct amount of stocks."); + assertEquals(listStocks.getFirst(), microStock, "Should return the correct first stock."); + assertEquals(listStocks.get(1), ntnuStock, "Should return the correct second stock."); + } + + @Test + void buyWithNullPlayerShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.buy( + "AAPL", new BigDecimal("1"), null), + "Should throw if player is null."); + } + + @Test + void buyWithNullQuantityShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.buy( + "AAPL", null, testPlayer), + "Should throw if quantity is null."); + } + + @Test + void buyWithZeroQuantityShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.buy( + "AAPL", BigDecimal.ZERO, testPlayer), + "Should throw if quantity is zero."); + } + + @Test + void buyWithNegativeQuantityShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.buy( + "AAPL", new BigDecimal("-1"), testPlayer), + "Should throw if quantity is negative."); + } + + @Test + void SellWithNullPlayerShouldThrowIllegalArgumentException() { + testPlayer.getPortfolio().addShare(appleShare); + + assertThrows(IllegalArgumentException.class, () -> exchange.sell(appleShare, null), + "Should throw if player is null."); + } + + + @Test + void sellWithNullShareShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> exchange.sell(null,testPlayer), + "Should throw if share is null."); + } + + @Test + void getCurrentWeekPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, exchange.getCurrentWeekProperty(), + "Should be Observable."); + assertEquals(exchange.getWeek(), exchange.getCurrentWeekProperty().getValue(), + "Should have correct week value."); + } + +} diff --git a/millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java similarity index 74% rename from millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java index 8e10787..d0cd50c 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java @@ -1,9 +1,12 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.*; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.List; +import javafx.beans.property.ObjectProperty; import static org.junit.jupiter.api.Assertions.*; @@ -335,13 +338,13 @@ void week1PlayerShouldHaveStatusNovice() { @Test void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() { String name = "Bob"; - var startingMoney = new BigDecimal("2000"); + var startingMoney = new BigDecimal("100"); var testPlayer = new Player(name, startingMoney); String symbol1 = "AAPL"; String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); + BigDecimal price1 = new BigDecimal("40"); Stock testStock1 = new Stock(symbol1,company1, price1); @@ -354,7 +357,6 @@ void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() { Exchange exchangeTest = new Exchange("test", stockList); - purchasePrice1 = new BigDecimal("3000"); Share testShare1 = new Share(testStock1, quantity1, purchasePrice1); testPlayer.getPortfolio().addShare(testShare1); @@ -365,8 +367,6 @@ void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() { assertEquals("Investor", testPlayer.getStatus(exchangeTest), "Since gain is over 20% and " + "week is 10, player should get status 'investor'."); - assertEquals("Investor", testPlayer.getStatus(exchangeTest), "Since gain is under 100% and " + - "week is 20, player should get status 'investor' and not 'speculator'."); } @Test @@ -443,4 +443,160 @@ void week20OrMorePlayerWithTwiceNetGainShouldHaveStatusSpeculator() { assertEquals("Speculator", testPlayer.getStatus(exchangeTest), "Should be Speculator as" + " the player has earned more than twice the starting money and the week is over 20."); } + + @Test + void Week20OrMorePlayerWithNoGainShouldStillHaveNoviceStatus() { + String symbol1 = "AAPL"; + String company1 = "Apple Inc."; + BigDecimal price1 = new BigDecimal("100"); + String name = "Bob"; + var startingMoney = new BigDecimal("2000"); + + Stock testStock1 = new Stock(symbol1,company1, price1); + List stockList = List.of(testStock1); + Exchange exchangeTest = new Exchange("test", stockList); + System.out.println(exchangeTest.getWeek()); + + for (int i = 0; i < 11; i++) { + exchangeTest.advance(); + } + + var testPlayer = new Player(name, startingMoney); + System.out.println(exchangeTest.getWeek()); + + assertEquals("Novice", testPlayer.getStatus(exchangeTest), "Should be Novice"); + } + + @Test + void lessThanWeek10WeekPlayerWithTwiceGainShouldHaveStatusNovice() { + String name = "Bob"; + var startingMoney = new BigDecimal("2000"); + + var testPlayer = new Player(name, startingMoney); + + String symbol1 = "AAPL"; + String company1 = "Apple Inc."; + BigDecimal price1 = new BigDecimal("100"); + + Stock testStock1 = new Stock(symbol1,company1, price1); + + BigDecimal quantity1 = new BigDecimal("10"); + var purchasePrice1 = testStock1.getSalesPrice(); + + Share testShare1 = new Share(testStock1, quantity1, purchasePrice1); + + String symbol2 = "MSFT"; + String company2 = "Microsoft Corporation"; + BigDecimal price2 = new BigDecimal("87"); + + Stock testStock2 = new Stock(symbol2, company2, price2); + + BigDecimal quantity2 = new BigDecimal("3"); + var purchasePrice2 = testStock1.getSalesPrice(); + + Share testShare2 = new Share(testStock2, quantity2, purchasePrice2); + + String symbol3 = "GOOG"; + String company3 = "Alphabet Inc."; + BigDecimal price3 = new BigDecimal("75"); + + Stock testStock3 = new Stock(symbol3, company3, price3); + + BigDecimal quantity3 = new BigDecimal("7"); + var purchasePrice3 = testStock3.getSalesPrice(); + + Share testShare3 = new Share(testStock3, quantity3, purchasePrice3); + + Portfolio testPortfolio = new Portfolio(); + testPortfolio.addShare(testShare1); + testPortfolio.addShare(testShare2); + testPortfolio.addShare(testShare3); + + testPlayer.getPortfolio().addShare(testShare1); + testPlayer.getPortfolio().addShare(testShare2); + testPlayer.getPortfolio().addShare(testShare3); + + for (int i = 0; i < 200; i++) { + testPlayer.getPortfolio().addShare(testShare3); + } + + var stockList = new ArrayList(); + + stockList.add(testStock1); + stockList.add(testStock2); + stockList.add(testStock3); + + Exchange exchangeTest = new Exchange("test", stockList); + + for (int i = 0; i < 5; i++) { + exchangeTest.advance(); + } + + assertEquals("Novice", testPlayer.getStatus(exchangeTest), + "Should be novice."); + } + + @Test + void week10OrMorePlayerWithLessThan20PercentGainShouldHaveStatusNovice() { + String name = "Bob"; + BigDecimal startingMoney = new BigDecimal("2000"); + + Player testPlayer = new Player(name, startingMoney); + + String symbol = "AAPL"; + String company = "Apple Inc."; + BigDecimal price = new BigDecimal("100"); + + Stock stock = new Stock(symbol, company, price); + + BigDecimal quantity = new BigDecimal("2"); + + Share share = new Share(stock, quantity, stock.getSalesPrice()); + + testPlayer.getPortfolio().addShare(share); + + Exchange exchange = new Exchange("test", List.of(stock)); + + for (int i = 0; i < 10; i++) { + exchange.advance(); + } + + assertEquals("Novice", testPlayer.getStatus(exchange)); + } + + @Test + void getNetWorthPropertyShouldReturnObservable() { + String name = "Bob"; + var startingMoney = new BigDecimal("2000"); + + String symbol = "AAPL"; + String company = "Apple Inc."; + BigDecimal price = new BigDecimal("100"); + + Stock stock = new Stock(symbol, company, price); + + BigDecimal quantity = new BigDecimal("2"); + Share share = new Share(stock, quantity, stock.getSalesPrice()); + Player testPlayer = new Player(name, startingMoney); + + testPlayer.getPortfolio().addShare(share); + + assertInstanceOf(ObjectProperty.class, testPlayer.getNetWorthProperty(), + "Should be Observable"); + assertEquals(testPlayer.getNetWorth(), testPlayer.getNetWorthProperty().getValue(), + "Should have correct net worth value."); + } + + @Test + void getMoneyPropertyShouldReturnObservable() { + String name = "Bob"; + var startingMoney = new BigDecimal("2000"); + + Player testPlayer = new Player(name, startingMoney); + + assertInstanceOf(ObjectProperty.class, testPlayer.getMoneyProperty(), + "Should be Observable."); + assertEquals(testPlayer.getMoney(), testPlayer.getMoneyProperty().getValue(), + "Should have correct money value."); + } } \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java similarity index 84% rename from millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java index 8534868..2943333 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java @@ -1,5 +1,9 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.Portfolio; +import no.ntnu.gruppe53.model.SaleCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -295,54 +299,21 @@ void containsShouldReturnFalseIfPortfolioDoesNotContainShare() { } @Test - void getNetWorthShouldReturnCorrectSaleValue() { - String symbol1 = "AAPL"; - String company1 = "Apple Inc."; - BigDecimal price1 = new BigDecimal("100"); - - Stock testStock1 = new Stock(symbol1,company1, price1); - - BigDecimal quantity1 = new BigDecimal("10"); - var purchasePrice1 = testStock1.getSalesPrice(); - - Share testShare1 = new Share(testStock1, quantity1, purchasePrice1); + void getNetWorthShouldReturnMarketValue() { - String symbol2 = "MSFT"; - String company2 = "Microsoft Corporation"; - BigDecimal price2 = new BigDecimal("87"); + Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100")); - Stock testStock2 = new Stock(symbol2, company2, price2); + Share share = new Share(stock, new BigDecimal("10"), stock.getSalesPrice()); - BigDecimal quantity2 = new BigDecimal("3"); - var purchasePrice2 = testStock1.getSalesPrice(); - - Share testShare2 = new Share(testStock2, quantity2, purchasePrice2); - - String symbol3 = "GOOG"; - String company3 = "Alphabet Inc."; - BigDecimal price3 = new BigDecimal("75"); - - Stock testStock3 = new Stock(symbol3, company3, price3); - - BigDecimal quantity3 = new BigDecimal("7"); - var purchasePrice3 = testStock3.getSalesPrice(); - - Share testShare3 = new Share(testStock3, quantity3, purchasePrice3); - - Portfolio testPortfolio = new Portfolio(); - - testPortfolio.addShare(testShare1); - testPortfolio.addShare(testShare2); - testPortfolio.addShare(testShare3); + Portfolio portfolio = new Portfolio(); + portfolio.addShare(share); - var calcWorth = BigDecimal.ZERO; - calcWorth = calcWorth.add(new SaleCalculator(testShare1).calculateTotal()); - calcWorth = calcWorth.add(new SaleCalculator(testShare2).calculateTotal()); - calcWorth = calcWorth.add(new SaleCalculator(testShare3).calculateTotal()); + BigDecimal expected = stock.getSalesPrice() + .multiply(share.getQuantity()); - assertEquals(0, testPortfolio.getNetWorth().compareTo(calcWorth), - "Calculated net worth of the portfolio should equal the " + - "som of all sales total values."); + assertEquals(0, + portfolio.getNetWorth().compareTo(expected), + "Net worth should equal market value of shares."); } @Test diff --git a/millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java similarity index 52% rename from millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java index 961cc9f..4935a80 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java @@ -1,11 +1,16 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import javafx.beans.property.ObjectProperty; +import no.ntnu.gruppe53.model.PurchaseCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; //The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works @@ -52,6 +57,36 @@ void testCalculateTotal() { } + @Test + void getGrossPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, purchaseCalculator.getGrossProperty(), + "Should be Observable."); + assertEquals(purchaseCalculator.calculateGross(), purchaseCalculator.getGrossProperty().getValue(), + "Should have the correct gross calculation."); + } + + @Test + void getCommissionPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, purchaseCalculator.getCommissionProperty(), + "Should be Observable."); + assertEquals(purchaseCalculator.calculateCommission(), purchaseCalculator.getCommissionProperty().getValue(), + "Should have the correct commission calculation."); + } + + @Test + void getTotalPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, purchaseCalculator.getTotalProperty(), + "Should be Observable."); + assertEquals(purchaseCalculator.calculateTotal(), purchaseCalculator.getTotalProperty().getValue(), + "Should have the correct total calculation."); + } + } diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java new file mode 100644 index 0000000..cfab01a --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java @@ -0,0 +1,64 @@ +package no.ntnu.gruppe53.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +class PurchaseFactoryTest { + private int week; + private Share share; + private TransactionFactory purchaseFactory; + private Transaction transaction; + + @BeforeEach + void setUp() { + String symbol = "AAPL"; + String company = "Apple Inc."; + var price = new BigDecimal("100"); + + var testStock = new Stock(symbol,company, price); + var quantity = new BigDecimal("10"); + var purchasePrice = testStock.getSalesPrice(); + + this.week = 1; + this.share = new Share(testStock, quantity, purchasePrice); + this.purchaseFactory = new PurchaseFactory(); + this.transaction = purchaseFactory.create(share, week); + } + + @Test + void createShouldMakeACorrectTransactionType() { + assertInstanceOf(Transaction.class, transaction, + "The returned value should be a transaction."); + assertInstanceOf(Purchase.class, transaction, + "Should be a purchase transaction."); + assertFalse(transaction instanceof Sale, + "Should not be a sale transaction."); + } + + @Test + void shouldHaveCorrectShareAndWeek() { + assertEquals(share, transaction.getShare(), + "Should contain the correct share."); + assertEquals(week, transaction.getWeek(), + "Should contain the correct week."); + } + + @Test + void shouldThrowIfShareIsNull() { + assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(null, week), + "Share should not be allowed to be null."); + } + + @Test + void shouldThrowIfWeekIsZeroOrBelow() { + assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(share, 0), + "Week should not be allowed to be zero"); + assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(share, -1), + "Week should not be allowed to be negative"); + } + +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java similarity index 98% rename from millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java index c6b5ba5..87c6563 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java @@ -1,5 +1,6 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.*; import org.junit.jupiter.api.Test; import java.math.BigDecimal; diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java new file mode 100644 index 0000000..fc0be79 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java @@ -0,0 +1,110 @@ +package no.ntnu.gruppe53.model; + +import javafx.beans.property.ObjectProperty; +import no.ntnu.gruppe53.model.SaleCalculator; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + + +//The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works + +@DisplayName("Sale Calculator Test") +public class SaleCalculatorTest { + + private Stock testStock = new Stock("AAPL", "Apple", new BigDecimal(100)); + + private Share testShare = new Share(testStock, new BigDecimal(10), new BigDecimal(40)); + + + private SaleCalculator saleCalculator = new SaleCalculator(testShare); + + @Test + @DisplayName("Sale Calculator: Calculate Gross") + void testCalculateGross() { + + BigDecimal expected = new BigDecimal(1000).setScale(2); + BigDecimal result = saleCalculator.calculateGross().setScale(2); + + assertEquals(expected, result); + + } + + @Test + @DisplayName("Commision of the sale transaction") + void testCalculateCommission() { + + BigDecimal expected = new BigDecimal(10).setScale(2); + BigDecimal result = saleCalculator.calculateCommission().setScale(2); + + assertEquals(expected, result); + + } + + @Test + @DisplayName("Tax of the sale transaction") + void testCalculateTax() { + + BigDecimal expected = new BigDecimal(177).setScale(2); + BigDecimal result = saleCalculator.calculateTax().setScale(2); + + assertEquals(expected, result); + + } + + @Test + @DisplayName("Total value of sale") + void testCalculateTotal() { + + BigDecimal expected = new BigDecimal(813).setScale(2); + BigDecimal result = saleCalculator.calculateTotal().setScale(2); + + assertEquals(expected, result); + + } + + @Test + void getGrossPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, saleCalculator.getGrossProperty(), + "Should be Observable."); + assertEquals(saleCalculator.calculateGross(), saleCalculator.getGrossProperty().getValue(), + "Should have the correct gross calculation."); + } + + @Test + void getCommissionPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, saleCalculator.getCommissionProperty(), + "Should be Observable."); + assertEquals(saleCalculator.calculateCommission(), saleCalculator.getCommissionProperty().getValue(), + "Should have the correct commission calculation."); + } + + @Test + void getTaxPropertyShouldReturnObservable() { + assertInstanceOf(ObjectProperty.class, saleCalculator.getTaxProperty(), + "Should be Observable."); + assertEquals(saleCalculator.calculateTax(), saleCalculator.getTaxProperty().getValue(), + "Should have the correct tax calculation."); + } + + @Test + void getTotalPropertyShouldReturnObservable() { + + + assertInstanceOf(ObjectProperty.class, saleCalculator.getTotalProperty(), + "Should be Observable."); + assertEquals(saleCalculator.calculateTotal(), saleCalculator.getTotalProperty().getValue(), + "Should have the correct total calculation."); + } + +} diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java new file mode 100644 index 0000000..3bfed18 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java @@ -0,0 +1,64 @@ +package no.ntnu.gruppe53.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +class SaleFactoryTest { + private int week; + private Share share; + private TransactionFactory saleFactory; + private Transaction transaction; + + @BeforeEach + void setUp() { + String symbol = "AAPL"; + String company = "Apple Inc."; + var price = new BigDecimal("100"); + + var testStock = new Stock(symbol,company, price); + var quantity = new BigDecimal("10"); + var purchasePrice = testStock.getSalesPrice(); + + this.week = 1; + this.share = new Share(testStock, quantity, purchasePrice); + this.saleFactory = new SaleFactory(); + this.transaction = saleFactory.create(share, week); + } + + @Test + void createShouldMakeACorrectTransactionType() { + assertInstanceOf(Transaction.class, transaction, + "The returned value should be a transaction."); + assertInstanceOf(Sale.class, transaction, + "Should be a sale transaction."); + assertFalse(transaction instanceof Purchase, + "Should not be a purchase transaction."); + } + + @Test + void shouldHaveCorrectShareAndWeek() { + assertEquals(share, transaction.getShare(), + "Should contain the correct share."); + assertEquals(week, transaction.getWeek(), + "Should contain the correct week."); + } + + @Test + void shouldThrowIfShareIsNull() { + assertThrows(IllegalArgumentException.class, () -> saleFactory.create(null, week), + "Share should not be allowed to be null."); + } + + @Test + void shouldThrowIfWeekIsZeroOrBelow() { + assertThrows(IllegalArgumentException.class, () -> saleFactory.create(share, 0), + "Week should not be allowed to be zero"); + assertThrows(IllegalArgumentException.class, () -> saleFactory.create(share, -1), + "Week should not be allowed to be negative"); + } + +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/SaleTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java similarity index 92% rename from millions/src/test/java/no/ntnu/gruppe53/SaleTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java index 2478d80..f53f921 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/SaleTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java @@ -1,5 +1,6 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.*; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -140,7 +141,9 @@ void saleShouldNotBeCommittedIfShareDoesNotExistInPortfolio() { var testSale = new Sale(testShare1, 1); - testSale.commit(testPlayer); + assertThrows(IllegalStateException.class, () -> { + testSale.commit(testPlayer); + }, "Sale should not be possible to commit as the player does not own the share."); assertEquals(0, startCash.compareTo(testPlayer.getMoney()), "Money should remain unchanged."); @@ -170,11 +173,10 @@ void saleShouldNotBeCommittedIfAlreadyCommited() { testSale.commit(testPlayer1); assertTrue(testSale.isCommitted(), "Sale should be committed."); - testSale.commit(testPlayer2); - - System.out.println(testPlayer1.getMoney()); - assertEquals(0, new BigDecimal("1093").compareTo(testPlayer1.getMoney()), + assertThrows(IllegalStateException.class, () -> testSale.commit(testPlayer2), + "Sale should not be committed a second time."); + assertEquals(0, new BigDecimal("1090").compareTo(testPlayer1.getMoney()), "Player 1 should have correct balance after sale."); assertEquals(0,startCash.compareTo(testPlayer2.getMoney()), "Money should not change for player 2."); @@ -204,8 +206,9 @@ void saleShouldNotBePossibleToCommitTwice() { testSale.commit(testPlayer); assertTrue(testSale.isCommitted(), "Sale should be committed."); BigDecimal cashAfterSale = testPlayer.getMoney(); - testSale.commit(testPlayer); + assertThrows(IllegalStateException.class, () -> testSale.commit(testPlayer), + "Sale should not be committed a second time."); assertEquals(0, cashAfterSale.compareTo(testPlayer.getMoney()), "Money should remain unchanged after second commit."); assertTrue(testSale.isCommitted(), "Sale should still be committed."); diff --git a/millions/src/test/java/no/ntnu/gruppe53/ShareTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java similarity index 97% rename from millions/src/test/java/no/ntnu/gruppe53/ShareTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java index 8e88b3b..1f76fc8 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/ShareTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java @@ -1,5 +1,7 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.Share; +import no.ntnu.gruppe53.model.Stock; import org.junit.jupiter.api.Test; import java.math.BigDecimal; diff --git a/millions/src/test/java/no/ntnu/gruppe53/StockTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java similarity index 94% rename from millions/src/test/java/no/ntnu/gruppe53/StockTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java index c9abf4c..8f89a22 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/StockTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java @@ -1,5 +1,6 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import no.ntnu.gruppe53.model.Stock; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -225,7 +226,7 @@ void getLowestPriceShouldReturnTheLowestPrice() { } @Test - void getLatestPriceChange() { + void getLatestPriceChangeShouldBeCorrect() { String symbol = "AAPL"; String company = "Apple Inc."; BigDecimal price1 = new BigDecimal("100"); @@ -239,8 +240,20 @@ void getLatestPriceChange() { testStock.addNewSalesPrice(price4); assertEquals(0, testStock.getLatestPriceChange().compareTo( - price4.subtract(price3)), + price4.subtract(price3)), "The price difference should be equal to the last value" + " subtracted from the second last value (i.e. around '-25'."); } + + @Test + void getLatestPriceChangeForPriceHistoryLessThan2ShouldGiveZero() { + String symbol = "AAPL"; + String company = "Apple Inc."; + BigDecimal price = new BigDecimal("100"); + + Stock testStock = new Stock(symbol,company, price); + + assertEquals(BigDecimal.ZERO, testStock.getLatestPriceChange(), + "Price change is zero for only 1 registered price."); + } } \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java similarity index 98% rename from millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java rename to millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java index 8912ab3..663babe 100644 --- a/millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java +++ b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java @@ -1,5 +1,6 @@ -package no.ntnu.gruppe53; +package no.ntnu.gruppe53.model; +import javafx.collections.ObservableList; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -468,4 +469,12 @@ void countDistinctWeeksShouldNotCountDuplicateWeekNumbers() { assertEquals(2, transactionArchive.countDistinctWeeks(), "Returned size of HashSet should be 2."); } + + @Test + void getObservableTransactionsShouldBeObservable() { + var transactionArchive = new TransactionArchive(); + + assertInstanceOf(ObservableList.class, transactionArchive.getObservableTransactions(), + "Should be Observable."); + } } \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java new file mode 100644 index 0000000..b0a09f1 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java @@ -0,0 +1,238 @@ +package no.ntnu.gruppe53.service; + +import no.ntnu.gruppe53.model.Stock; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FileHandlerTest { + + private Path tempDir; + + @BeforeEach + void setup() throws Exception { + tempDir = Files.createTempDirectory("stock_test_"); + } + + @AfterEach + void cleanup() throws Exception { + if (tempDir != null && Files.exists(tempDir)) { + Files.walk(tempDir) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + void writingAndReadingStocksFromListShouldYieldIdenticalStockList() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + + String filename = "testStocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(stocks.size(), readStocks.size(), "Stock list size should match"); + for (int i = 0; i < stocks.size(); i++) { + Stock expected = stocks.get(i); + Stock actual = readStocks.get(i); + assertEquals(expected.getSymbol(), actual.getSymbol()); + assertEquals(expected.getCompany(), actual.getCompany()); + assertEquals(0, expected.getSalesPrice().compareTo(actual.getSalesPrice())); + } + } + + @Test + void writerShouldIgnoreNullStockObjects() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, null, stock2); + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), "stocks.csv"); + List writtenStocks = FileHandler.readStocksFromFile(tempDir.toString(),"stocks.csv"); + + assertEquals(3, stocks.size(), "Stock list should include null stock."); + assertEquals(stocks.size()-1, writtenStocks.size(), + "Null stock should be ignored by the writer."); + + } + + @Test + void writeFileNameWithoutExtensionShouldAddExtension() { + + List stocks = List.of( + new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00")) + ); + + String filenameWithoutExtension = "stocks"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithoutExtension); + + Path expectedFile = tempDir.resolve("stocks.csv"); + + assertTrue(Files.exists(expectedFile), "File should end with .csv extension."); + } + + @Test + void writeToNonExistingDirectoriesShouldCreateTheDirectories() { + Path nonExistingDir = tempDir.resolve("subdir1/subdir2"); + + List stocks = List.of( + new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00")) + ); + + FileHandler.writeStocksToFile(stocks, nonExistingDir.toString(), "stocks"); + + Path expectedFile = nonExistingDir.resolve("stocks.csv"); + + assertTrue(Files.exists(expectedFile), "File should be created in newly created directories"); + assertTrue(Files.exists(nonExistingDir), "Directories should have been created"); + } + + @Test + void writeEmptyListShouldReturnEmptyList() { + List emptyList = List.of(); + String filename = "emptyStocks.csv"; + + FileHandler.writeStocksToFile(emptyList, tempDir.toString(), filename); + + List result = FileHandler.readStocksFromFile(tempDir.toString(), filename); + assertTrue(result.isEmpty(), "Reading empty stock list should return empty list"); + } + + @Test + void writeInvalidStockListShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(null, tempDir.toString(), "file.csv")); + } + + @Test + void writeInvalidFolderPathShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), " ", "file.csv")); + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)),null, "file.csv")); + } + + @Test + void writeInvalidFileNameShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), " ")); + assertThrows(IllegalArgumentException.class, () -> + FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), null)); + } + + @Test + void readStocksShouldIgnoreMalformedLines() throws Exception { + String filename = "malformedStocks.csv"; + Path filePath = tempDir.resolve(filename); + + List lines = List.of( + "# This is a comment", + "AAPL,Apple Inc.,150.25", + "INVALIDLINEWITHOUTCOMMAS", + "MSFT,Microsoft Corp.,300.50" + ); + + Files.write(filePath, lines, StandardCharsets.UTF_8); + + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(2, stocks.size(), "Only 2 valid stocks should be read"); + + assertEquals("AAPL", stocks.getFirst().getSymbol()); + assertEquals("Apple Inc.", stocks.getFirst().getCompany()); + assertEquals(0, stocks.getFirst().getSalesPrice().compareTo(new BigDecimal("150.25"))); + + assertEquals("MSFT", stocks.get(1).getSymbol()); + assertEquals("Microsoft Corp.", stocks.get(1).getCompany()); + assertEquals(0, stocks.get(1).getSalesPrice().compareTo(new BigDecimal("300.50"))); + } + + @Test + void readStocksShouldCatchExceptionsFromIllegalStockParameters() throws IOException { + String filename = "malformedStocks.csv"; + Path filePath = tempDir.resolve(filename); + + List lines = List.of( + "GOOG,Google,,", + "GGL, , 200", + " , Google, 100", + "GGL, Google, A", + " ", + "TSLA,Tesla Inc.,-100" + ); + Files.write(filePath, lines, StandardCharsets.UTF_8); + + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(0, stocks.size(), "No stocks with illegal parameters should be added."); + } + + @Test + void readStocksShouldThrowIllegalArgumentExceptionIfPathIsNullOrBlank() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(null, filename), + "Null path should not be allowed."); + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(" ", filename), + "Blank path should not be allowed."); + } + + @Test + void readStocksShouldThrowIllegalArgumentExceptionIfFileNameIsNullOrBlank() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks.csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename); + + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile( + tempDir.toString(), null), + "Null filename should not be allowed."); + assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile( + tempDir.toString(), " "), + "Blank filename should not be allowed."); + } + + @Test + void readShouldAddExtensionIfMissingFromFileName() { + Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25")); + Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50")); + List stocks = Arrays.asList(stock1, stock2); + String filename = "stocks"; + String filenameWithExtension = filename + ".csv"; + + FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithExtension); + List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename); + + assertEquals(stocks.size(), readStocks.size(), "All stocks should be read successfully."); + } + + @Test + void readerShouldReturnEmptyListIfFileIsMissing() { + List stocks = FileHandler.readStocksFromFile(tempDir.toString(), "stocklist.csv"); + + assertEquals(0, stocks.size(), "Stock list should be empty."); + } +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java new file mode 100644 index 0000000..9997a01 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java @@ -0,0 +1,28 @@ +package no.ntnu.gruppe53.service; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +class FormatBigDecimalTest { + @Test + void shouldReturnZeroIfBigDecimalIsNull() { + assertEquals("0.00", FormatBigDecimal.formatNumber(null), + "Null should be formatted as '0.00'."); + } + + @Test + void numberShouldOnlyHaveTwoDecimals() { + assertEquals("2.20", FormatBigDecimal.formatNumber(new BigDecimal("2.2000000000001")), + "Number should only have two decimals."); + } + + @Test + void numberShouldRoundUp() { + assertEquals("32.20", FormatBigDecimal.formatNumber(new BigDecimal("32.1967")), + "Number should round up."); + } + +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java new file mode 100644 index 0000000..4f04ae2 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java @@ -0,0 +1,68 @@ +package no.ntnu.gruppe53.service; + +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageManagerTest { + + @Test + void constructorShouldBePrivate() throws Exception { + Constructor constructor = + LanguageManager.class.getDeclaredConstructor(); + + assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers())); + } + + @Test + void instanceShouldBeUniform() { + var lm1 = LanguageManager.getInstance(); + var lm2 = LanguageManager.getInstance(); + + assertEquals(lm1, lm2, "The instance should be unchanged."); + } + + @Test + void shouldLoadStringsCorrectly() { + LanguageManager manager = LanguageManager.getInstance(); + + manager.setLocale(new Locale("en")); + + String value = manager.getString("hello"); + + assertEquals("Hello world", value); + } + + @Test + void shouldReturnCorrectLocale() { + LanguageManager manager = LanguageManager.getInstance(); + + manager.setLocale(new Locale("en")); + + assertEquals(new Locale("en"), manager.getLocale()); + } + + @Test + void getBundlePropertyShouldBeObservable() { + assertInstanceOf(ObjectProperty.class, LanguageManager.getInstance().getBundleProperty()); + } + + @Test + void bindStringShouldBeReactiveOnLocaleChange() { + LanguageManager lm = LanguageManager.getInstance(); + + lm.setLocale(new Locale("en")); + StringBinding binding = lm.bindString("hello"); + + assertEquals("Hello world", binding.get(), "Should return english version of key."); + + lm.setLocale(new java.util.Locale("no")); + + assertEquals("Hallo verden", binding.get(), "Should return norwegian version of key."); + } +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java new file mode 100644 index 0000000..ab72057 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java @@ -0,0 +1,37 @@ +package no.ntnu.gruppe53.service; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageRegistryTest { + @Test + void getLanguageShouldReturnCorrectAmountAndCorrectLanguages() { + var languages = LanguageRegistry.getLanguages(); + boolean hasEnglish = languages.stream() + .anyMatch(lang -> lang.locale().equals(new Locale("en"))); + boolean hasNorwegian = languages.stream() + .anyMatch(lang -> lang.locale().equals(new Locale("no"))); + + assertEquals(2, languages.size(), "Should be 2 languages."); + assertTrue(hasEnglish, "English should be a supported language."); + assertTrue(hasNorwegian, "Norwegian should be a supported language."); + } + + @Test + void getDefaultLanguageShouldEqualEnglish() { + assertEquals("English", LanguageRegistry.getDefaultLanguage().name(), + "Default language name should be 'English'."); + assertEquals(new Locale("en"), LanguageRegistry.getDefaultLanguage().locale(), + "Default language locale should be 'en'."); + } + + @Test + void getDefaultLocaleShouldReturnEnglish() { + assertEquals(new Locale("en"), LanguageRegistry.getDefaultLocale(), + "Default language locale should be 'en'."); + } + +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java new file mode 100644 index 0000000..9c94dad --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java @@ -0,0 +1,182 @@ +package no.ntnu.gruppe53.service; +import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.model.Stock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +class StockLineChartServiceTest { + private Stock appleStock; + + private Stock microStock; + + private Stock ntnuStock; + + private ArrayList stocks; + + private Exchange exchange; + + @BeforeEach + void setUp() { + appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); + microStock = new Stock("MSFT", "Microsoft", new BigDecimal("50")); + ntnuStock = new Stock("NTNU", "Ntnu", new BigDecimal("1000")); + + stocks = new ArrayList<>(); + stocks.add(appleStock); + stocks.add(microStock); + stocks.add(ntnuStock); + + exchange = new Exchange("Nasdaq", stocks); + } + + @Test + void shouldThrowExceptionIfPriceAndWeekIsNotOneToOne() { + exchange.advance(); + + appleStock.addNewSalesPrice(new BigDecimal("50")); + + assertThrows(IllegalArgumentException.class, () -> + StockLineChartService.buildSeries(appleStock, exchange), + "Stock should not be allowed to have more values than weeks in exchange for plotting chart."); + } + + @Test + void buildSeriesShouldHaveCorrectWeekForOneStock() { + for (int i = 0; i < 9; i++) { + exchange.advance(); + } + + var series = StockLineChartService.buildSeries(appleStock, exchange); + double week = series.getData().stream().mapToDouble(data -> data.getXValue() + .doubleValue()).max().orElse(0); + + + assertEquals(exchange.getWeek(), week, "Max week number should be correct."); + } + + @Test + void buildSeriesShouldHaveCorrectWeekForMultipleStocks() { + for (int i = 0; i < 4; i++) { + exchange.advance(); + } + + var series = StockLineChartService.buildSeries(stocks, exchange); + double week = exchange.getWeek(); + double weekFirstStock = series.getFirst().getData().stream() + .mapToDouble(data -> data.getXValue().doubleValue()) + .max().orElse(0); + double weekSecondStock = series.get(1).getData().stream() + .mapToDouble(data -> data.getXValue().doubleValue()) + .max().orElse(0); + double weekLastStock = series.getLast().getData().stream() + .mapToDouble(data -> data.getXValue().doubleValue()) + .max().orElse(0); + + assertEquals(week, weekFirstStock, "Week value for first stock in series should be correct."); + assertEquals(week, weekSecondStock, "Week value for second stock in series should be correct."); + assertEquals(week, weekLastStock, "Week value for last stock in series should be correct."); + } + + @Test + void buildSeriesShouldHaveCorrectPricesForOneStock() { + exchange.advance(); + + var series = StockLineChartService.buildSeries(appleStock, exchange); + + double firstPrice = appleStock.getHistoricalPrices().getFirst().doubleValue(); + double secondPrice = exchange.getStock(appleStock.getSymbol()).getSalesPrice().doubleValue(); + double firstPriceSeries = series.getData().stream().mapToDouble( + data -> data.getYValue().doubleValue()).findFirst().orElse(0); + double secondPriceSeries = series.getData().stream().mapToDouble( + data -> data.getYValue().doubleValue()).skip(1). + findFirst().orElse(0); + + assertEquals(firstPrice, firstPriceSeries, "First price should be the same."); + assertEquals(secondPrice, secondPriceSeries, "Second price should be the same."); + } + + @Test + void buildSeriesShouldHaveCorrectPricesForMultipleStocks() { + exchange.advance(); + + var series = StockLineChartService.buildSeries(stocks, exchange); + + // First stock prices + double firstPriceFirstStock = exchange.getStock(appleStock.getSymbol()) + .getHistoricalPrices().getFirst().doubleValue(); + double firstPriceFirstStockSeries = series.getFirst().getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .findFirst().orElse(0); + double lastPriceFirstStock = exchange.getStock(appleStock.getSymbol()).getSalesPrice().doubleValue(); + double lastPriceFirstStockSeries =series.getFirst().getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .skip(1).findFirst().orElse(0); + + // Second stock prices + double firstPriceSecondStock = exchange.getStock(microStock.getSymbol()) + .getHistoricalPrices().getFirst().doubleValue(); + double firstPriceSecondStockSeries = series.get(1).getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .findFirst().orElse(0); + double lastPriceSecondStock = exchange.getStock(microStock.getSymbol()).getSalesPrice().doubleValue(); + double lastPriceSecondStockSeries =series.get(1).getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .skip(1).findFirst().orElse(0); + + // Last stock prices + double firstPriceLastStock = exchange.getStock(ntnuStock.getSymbol()) + .getHistoricalPrices().getFirst().doubleValue(); + double firstPriceLastStockSeries = series.getLast().getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .findFirst().orElse(0); + double lastPriceLastStock = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice().doubleValue(); + double lastPriceLastStockSeries =series.getLast().getData().stream() + .mapToDouble(data -> data.getYValue().doubleValue()) + .skip(1).findFirst().orElse(0); + + assertEquals(firstPriceFirstStock, firstPriceFirstStockSeries, + "First price for first stock should be correct."); + assertEquals(lastPriceFirstStock, lastPriceFirstStockSeries, + "Last price for first stock should be correct."); + assertEquals(firstPriceSecondStock, firstPriceSecondStockSeries, + "First price for second stock should be correct."); + assertEquals(lastPriceSecondStock, lastPriceSecondStockSeries, + "Last price for second stock should be correct."); + assertEquals(firstPriceLastStock, firstPriceLastStockSeries, + "First price for last stock should be correct."); + assertEquals(lastPriceLastStock, lastPriceLastStockSeries, + "Last price for last stock should be correct."); + } + + @Test + void buildSeriesShouldHaveCorrectSymbolForOneStock() { + var series = StockLineChartService.buildSeries(appleStock, exchange); + + String name = appleStock.getSymbol(); + String nameSeries = series.getName(); + + assertEquals(name, nameSeries, "Series should have correct stock symbol."); + } + + @Test + void buildSeriesShouldHaveCorrectSymbolForMultipleStocks() { + var series = StockLineChartService.buildSeries(stocks, exchange); + + String firstStockName = appleStock.getSymbol(); + String firstStockNameSeries = series.getFirst().getName(); + String secondStockName = microStock.getSymbol(); + String secondStockNameSeries = series.get(1).getName(); + String lastStockName = ntnuStock.getSymbol(); + String lastStockNameSeries = series.getLast().getName(); + + assertEquals(firstStockName, firstStockNameSeries, "First stock name should be correct."); + assertEquals(secondStockName, secondStockNameSeries, "Second stock name should be correct."); + assertEquals(lastStockName, lastStockNameSeries, "Last stock name should be correct."); + } +} \ No newline at end of file diff --git a/millions/src/test/resources/i18n/lang_en.properties b/millions/src/test/resources/i18n/lang_en.properties new file mode 100644 index 0000000..6d5d7ab --- /dev/null +++ b/millions/src/test/resources/i18n/lang_en.properties @@ -0,0 +1 @@ +hello = Hello world \ No newline at end of file diff --git a/millions/src/test/resources/i18n/lang_no.properties b/millions/src/test/resources/i18n/lang_no.properties new file mode 100644 index 0000000..05bdb92 --- /dev/null +++ b/millions/src/test/resources/i18n/lang_no.properties @@ -0,0 +1 @@ +hello = Hallo verden \ No newline at end of file diff --git a/millions/target/classes/Millions-logo.png b/millions/target/classes/Millions-logo.png new file mode 100644 index 0000000..328546f Binary files /dev/null and b/millions/target/classes/Millions-logo.png differ diff --git a/millions/target/classes/Scroll-dot.png b/millions/target/classes/Scroll-dot.png new file mode 100644 index 0000000..eaae1d9 Binary files /dev/null and b/millions/target/classes/Scroll-dot.png differ diff --git a/millions/target/classes/i18n/lang.properties b/millions/target/classes/i18n/lang.properties new file mode 100644 index 0000000..87c134e --- /dev/null +++ b/millions/target/classes/i18n/lang.properties @@ -0,0 +1,83 @@ +# File for the english language + +# General +yes = Yes +no = No +search = Search: + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History +statusLabelText = Level: +endGameButton = End Game + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week +biggestLoserLabelText = Biggest Loser: +biggestWinnerLabelText = Biggest Winner: + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +portfolioProfit = Profit: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Price: %s | Qty: %s | Gross: %s | Commission: %s | Tax: %s | Total: $%s | Profit: %s + +# EndView +congratsLabel = Thank you for playing Millions!\nBelow you can see what level you reached and the amount of\nmoney you ended up with!\nYou can also choose to start a new game. + +# EndGameAlert +endGameAlertTitle = Ending Game +endGameAlertHeaderText=Warning, this ends the game! +endGameAlertContentText = Are your sure you want to end the game? + + + diff --git a/millions/target/classes/i18n/lang_en.properties b/millions/target/classes/i18n/lang_en.properties new file mode 100644 index 0000000..1c8ef53 --- /dev/null +++ b/millions/target/classes/i18n/lang_en.properties @@ -0,0 +1,80 @@ +# File for the english language + +# General +yes = Yes +no = No +search = Search: + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History +statusLabelText = Level: +endGameButton = End Game + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week +biggestLoserLabelText = Biggest Loser: +biggestWinnerLabelText = Biggest Winner: + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +portfolioProfit = Profit: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Price: %s | Qty: %s | Gross: %s | Commission: %s | Tax: %s | Total: $%s | Profit: %s + +# EndView +congratsLabel = Thank you for playing Millions!\nBelow you can see what level you reachedand the amount of\nmoney you ended up with!\nYou can also choose to start a new game. + +# EndGameAlert +endGameAlertTitle = Ending Game +endGameAlertHeaderText=Warning, this ends the game! +endGameAlertContentText = Are your sure you want to end the game? diff --git a/millions/target/classes/i18n/lang_no.properties b/millions/target/classes/i18n/lang_no.properties new file mode 100644 index 0000000..b5deee0 --- /dev/null +++ b/millions/target/classes/i18n/lang_no.properties @@ -0,0 +1,81 @@ +# File for the norwegian language + +# General +yes = Ja +no = Nei +search = Søk: + +# StartView +newGame = Nytt Spill +languageButtonLabel = Skift Språk +quitGame = Avslutt + +# PlayerNameChooserView +playerNameConfirmButton = Bekreft +playerNameInfo = Vennligst skriv spillernavnet ditt: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Bekreft +playerStartingMoneyInfoLabel = Velg startsaldo for aksjehandel: + +# CSVChooserView +csvHeader = CSV Fil Påkrevd +csvContent = Vennligst velg en .csv fil som inneholder aksjene som skal brukes på børsen i neste vindu. \n\nDersom ingen fil velges (eller .csv filen er tom eller har feil struktur), vil en standard fil bli brukt. \n\nFilen må formates slik: Aksjesymbol,FirmaNavn,PrisTypeDobbel. \n\nEksempel: NVDA,Nvidia,191.27\n\n'#' brukes for kommentarer, og som med feilformaterte linjer, vil slike linjer bli ignorert. +csvFileChooserTitle = Velg CSV fil +csvFileFilterLabel = CSV Filer + +# NavigationBar +navMoneyLabelText = Saldo: +navNetWorthLabelText = Nettoformue: +navPlayerLabelText = Spiller: +marketButton = Marked +portfolioButton = Portefølje +historyButton = Historikk +statusLabelText = Nivå: +endGameButton = Avslutt Spill + +# FooterBar +currentWeekLabelText = Gjeldende Uke: +advanceWeekButton = Neste Uke +biggestLoserLabelText = Største Taper: +biggestWinnerLabelText = Største Vinner: + +# MarketView +marketTitle = Aksjemarked +xAxisLabel = Uke +yAxisLabel = Pris +selectedStock = Valgt aksje: +purchaseQuantity = Antall: +purchaseGross = Brutto: +purchaseCommission = Kurtasje: +purchaseTotal = Total: +purchaseButton = Kjøp +stockCellFormat=%s - %s | Pris: $%s | Prisendring: %s | Høyeste pris: %s | Laveste pris: %s + +# PortfolioView +portfolioTitle = Portefølje +selectedShare = Valgt andel: +portfolioGross = Brutto: +portfolioCommission = Kurtasje: +portfolioTax = Skatt: +portfolioTotal = Total: +portfolioProfit = Profitt: +sellButton = Selg +portfolioShareQuantity = Ant: +portfolioBuyPrice = Kjøpspris: +portfolioCurrentPrice = Gjeldende pris: + +# TransactionArchiveView +historyTitle=Transaksjonshistorikk +historyTypePurchase=Kjøp +historyTypeSale=Salg +historyCellFormat=Uke %d | %s | %s | Pris: %s | Ant: %s | Brutto: %s | Kurtasje: %s | Skatt: %s | Total: $%s | Profit: %s + + +# EndView +congratsLabel = Takk for at du spilte Millions!\nNedenfor kan du se hvilket nivå du nådde opp til\nog din endelige saldo!\nDu kan også velge å starte et nytt spill. + +# EndGameAlert +endGameAlertTitle = Avslutter Spill +endGameAlertHeaderText= Advarsel, dette vil avslutte spillet! +endGameAlertContentText = Er du sikker på at du vil avslutte spillet? \ No newline at end of file diff --git a/millions/target/classes/sp500.csv b/millions/target/classes/sp500.csv new file mode 100644 index 0000000..d9cec61 --- /dev/null +++ b/millions/target/classes/sp500.csv @@ -0,0 +1,506 @@ +# S&P 500 Companies by Market Cap +# Ticker,Name,Price + +NVDA,Nvidia,191.27 +AAPL,Apple Inc.,276.43 +MSFT,Microsoft,404.68 +AMZN,Amazon,204.62 +GOOGL,Alphabet Inc. (Class A),311.20 +GOOG,Alphabet Inc. (Class C),311.62 +META,Meta Platforms,669.41 +AVGO,Broadcom,343.35 +TSLA,Tesla Inc.,426.52 +BRK.B,Berkshire Hathaway,501.05 +WMT,Walmart,128.75 +LLY,Lilly (Eli),1014.43 +JPM,JPMorgan Chase,311.14 +XOM,ExxonMobil,155.28 +V,Visa Inc.,329.54 +JNJ,Johnson & Johnson,240.70 +MA,Mastercard,539.52 +MU,Micron Technology,411.25 +ORCL,Oracle Corporation,157.08 +COST,Costco,979.71 +BAC,Bank of America,54.06 +ABBV,AbbVie,220.17 +HD,Home Depot (The),389.46 +PG,Procter & Gamble,159.45 +CVX,Chevron Corporation,185.66 +CAT,Caterpillar Inc.,773.53 +AMD,Advanced Micro Devices,213.00 +CSCO,Cisco,85.82 +KO,Coca-Cola Company (The),78.51 +NFLX,Netflix,79.94 +GE,GE Aerospace,314.37 +PLTR,Palantir Technologies,135.59 +LRCX,Lam Research,236.60 +MRK,Merck & Co.,118.79 +PM,Philip Morris International,185.99 +GS,Goldman Sachs,949.29 +MS,Morgan Stanley,176.86 +WFC,Wells Fargo,89.07 +AMAT,Applied Materials,342.19 +RTX,RTX Corporation,197.56 +IBM,IBM,273.86 +UNH,UnitedHealth Group,278.79 +AXP,American Express,355.18 +INTC,Intel,47.85 +TMUS,T-Mobile US,207.62 +PEP,PepsiCo,168.71 +MCD,McDonald's,323.50 +GEV,GE Vernova,822.50 +LIN,Linde plc,466.04 +C,Citigroup,117.93 +TXN,Texas Instruments,226.34 +VZ,Verizon,48.55 +T,AT&T,28.21 +TMO,Thermo Fisher Scientific,525.00 +AMGN,Amgen,364.77 +ABT,Abbott Laboratories,112.97 +KLAC,KLA Corporation,1492.27 +GILD,Gilead Sciences,155.71 +DIS,Walt Disney Company (The),108.30 +NEE,NextEra Energy,91.19 +BA,Boeing,236.98 +ANET,Arista Networks,141.06 +APH,Amphenol,144.60 +ISRG,Intuitive Surgical,496.14 +CRM,Salesforce,184.94 +SCHW,Charles Schwab Corporation,95.50 +BLK,BlackRock,1083.35 +TJX,TJX Companies,150.56 +DE,Deere & Company,610.03 +ADI,Analog Devices,336.71 +LOW,Lowe's,286.57 +PFE,Pfizer,27.77 +UNP,Union Pacific Corporation,261.95 +DHR,Danaher Corporation,219.80 +APP,AppLovin Corporation,459.27 +HON,Honeywell,243.56 +ETN,Eaton Corporation,394.94 +QCOM,Qualcomm,141.90 +UBER,Uber,70.72 +LMT,Lockheed Martin,630.54 +WELL,Welltower,208.65 +ACN,Accenture,230.79 +BKNG,Booking Holdings,4322.85 +SYK,Stryker Corporation,362.53 +COP,ConocoPhillips,110.75 +NEM,Newmont,123.89 +COF,Capital One,214.93 +PLD,Prologis,140.35 +MDT,Medtronic,100.87 +CB,Chubb Limited,328.82 +PH,Parker Hannifin,998.24 +PGR,Progressive Corporation,209.33 +BMY,Bristol Myers Squibb,60.18 +HCA,HCA Healthcare,531.83 +SPGI,S&P Global,396.65 +CMCSA,Comcast,32.53 +VRTX,Vertex Pharmaceuticals,459.93 +MCK,McKesson Corporation,944.20 +PANW,Palo Alto Networks,165.49 +GLW,Corning Inc.,134.16 +SBUX,Starbucks,99.03 +INTU,Intuit,401.05 +MO,Altria,65.72 +BSX,Boston Scientific,73.76 +CME,CME Group,303.44 +NOW,ServiceNow,101.55 +ADBE,Adobe Inc.,258.39 +TT,Trane Technologies,473.75 +CRWD,CrowdStrike,414.94 +BX,Blackstone Inc.,133.29 +UPS,United Parcel Service,119.93 +SO,Southern Company,90.86 +CEG,Constellation Energy,274.37 +DUK,Duke Energy,124.86 +CVS,CVS Health,76.36 +MAR,Marriott International,360.71 +NOC,Northrop Grumman,680.45 +PNC,PNC Financial Services,237.28 +WM,Waste Management,234.67 +GD,General Dynamics,348.79 +WDC,Western Digital,277.26 +KKR,KKR,105.28 +HWM,Howmet Aerospace,233.13 +FCX,Freeport-McMoRan,65.30 +NKE,Nike Inc.,62.43 +USB,U.S. Bancorp,59.25 +MMM,3M,173.46 +SHW,Sherwin-Williams,364.16 +RCL,Royal Caribbean Group,331.76 +SNDK,Sandisk Corporation,607.86 +STX,Seagate Technology,409.41 +EMR,Emerson Electric,156.30 +ADP,Automatic Data Processing,217.59 +WMB,Williams Companies,71.48 +ICE,Intercontinental Exchange,153.60 +FDX,FedEx,368.49 +ITW,Illinois Tool Works,298.50 +JCI,Johnson Controls,140.75 +CRH,CRH plc,127.89 +ECL,Ecolab,301.35 +EQIX,Equinix,863.66 +BK,BNY Mellon,122.83 +MRSH,Marsh & McLennan Companies Inc.,174.09 +AMT,American Tower,179.46 +CMI,Cummins,601.45 +SNPS,Synopsys,433.56 +REGN,Regeneron Pharmaceuticals,780.09 +DELL,Dell Technologies,124.37 +CDNS,Cadence Design Systems,298.74 +CTAS,Cintas,201.10 +ORLY,O'Reilly Auto Parts,93.87 +MNST,Monster Beverage,80.88 +MDLZ,Mondelez International,61.45 +PWR,Quanta Services,523.69 +CI,Cigna,292.46 +CSX,CSX Corporation,41.30 +CL,Colgate-Palmolive,95.04 +SLB,Schlumberger,51.14 +HLT,Hilton Worldwide,327.38 +DASH,DoorDash,175.41 +TDG,TransDigm Group,1325.26 +MCO,Moody's Corporation,415.20 +APO,Apollo Global Management,127.53 +ELV,Elevance Health,329.59 +ABNB,Airbnb,119.56 +GM,General Motors,79.78 +NSC,Norfolk Southern Railway,316.73 +COR,Cencora,365.43 +MSI,Motorola Solutions,423.10 +KMI,Kinder Morgan,31.64 +RSG,Republic Services,226.72 +HOOD,Robinhood Markets Inc.,77.55 +WBD,Warner Bros. Discovery,28.01 +TFC,Truist Financial,54.42 +PCAR,Paccar,129.93 +AON,Aon,314.02 +TEL,TE Connectivity,227.16 +APD,Air Products,293.38 +AEP,American Electric Power,122.18 +FTNT,Fortinet,87.72 +TRV,Travelers Companies (The),299.75 +PSX,Phillips 66,161.13 +LHX,L3Harris,341.14 +EOG,EOG Resources,117.39 +SPG,Simon Property Group,195.66 +NXPI,NXP Semiconductors,249.26 +ROST,Ross Stores,192.31 +VLO,Valero Energy,203.89 +AZO,AutoZone,3733.09 +MPC,Marathon Petroleum,207.85 +BKR,Baker Hughes,61.16 +AFL,Aflac,116.20 +DLR,Digital Realty,174.16 +SRE,Sempra,90.83 +O,Realty Income,64.39 +MPWR,Monolithic Power Systems,1197.55 +GWW,W. W. Grainger,1202.13 +ZTS,Zoetis,128.19 +CARR,Carrier Global,66.80 +D,Dominion Energy,64.61 +F,Ford Motor Company,13.78 +URI,United Rentals,870.17 +AME,Ametek,236.33 +VST,Vistra Corp.,160.43 +FAST,Fastenal,47.14 +ALL,Allstate,205.91 +OKE,ONEOK,85.03 +AJG,Arthur J. Gallagher & Co.,207.61 +CAH,Cardinal Health,225.15 +CVNA,Carvana Co.,365.94 +IDXX,Idexx Laboratories,647.63 +MET,MetLife,78.87 +TGT,Target Corporation,114.12 +PSA,Public Storage,293.33 +BDX,Becton Dickinson,179.62 +CTVA,Corteva,75.48 +TER,Teradyne,323.92 +EA,Electronic Arts,201.72 +ADSK,Autodesk,232.93 +FITB,Fifth Third Bancorp,54.59 +CMG,Chipotle Mexican Grill,37.35 +FANG,Diamondback Energy,168.93 +TRGP,Targa Resources,222.03 +FIX,Comfort Systems USA Inc.,1345.62 +DHI,D. R. Horton,163.35 +HSY,Hershey Company (The),231.46 +OXY,Occidental Petroleum,47.34 +DAL,Delta Air Lines,71.16 +ROK,Rockwell Automation,413.43 +NDAQ,Nasdaq Inc.,81.05 +XEL,Xcel Energy,77.79 +EW,Edwards Lifesciences,78.67 +CCL,Carnival,32.80 +CBRE,CBRE Group,151.76 +ETR,Entergy,100.79 +EXC,Exelon,44.57 +AMP,Ameriprise Financial,489.27 +NUE,Nucor,194.78 +DDOG,Datadog,126.70 +YUM,Yum! Brands,160.35 +MCHP,Microchip Technology,80.90 +WAB,Wabtec,255.15 +KR,Kroger,68.68 +AIG,American International Group,79.36 +VMC,Vulcan Materials Company,321.01 +CIEN,Ciena Corporation,301.11 +SYY,Sysco,88.04 +PEG,Public Service Enterprise Group,83.85 +COIN,Coinbase Global,152.61 +ODFL,Old Dominion,195.74 +KEYS,Keysight Technologies,237.88 +KDP,Keurig Dr Pepper,29.84 +VTR,Ventas,85.29 +MLM,Martin Marietta Materials,663.38 +GRMN,Garmin,206.52 +ED,Consolidated Edison,109.36 +HIG,Hartford (The),142.02 +LVS,Las Vegas Sands,57.75 +CPRT,Copart,39.73 +EL,Estée Lauder Companies (The),106.09 +IR,Ingersoll Rand,96.98 +WDAY,Workday Inc.,145.19 +MSCI,MSCI,519.16 +TTWO,Take-Two Interactive,204.25 +RMD,ResMed,259.29 +EBAY,eBay,82.95 +PCG,PG&E Corporation,17.02 +CCI,Crown Castle,85.66 +PYPL,PayPal,40.26 +PRU,Prudential Financial,105.34 +WEC,WEC Energy Group,113.19 +UAL,United Airlines Holdings,113.50 +STT,State Street Corporation,131.37 +HBAN,Huntington Bancshares,18.02 +A,Agilent Technologies,128.22 +GEHC,GE HealthCare,79.29 +MTB,M&T Bank,235.06 +EME,EMCOR Group Inc.,803.53 +ACGL,Arch Capital Group,98.49 +KMB,Kimberly-Clark,107.35 +ROP,Roper Technologies,333.96 +EQT,EQT Corporation,56.73 +KVUE,Kenvue,18.44 +LYV,Live Nation Entertainment,150.60 +OTIS,Otis Worldwide,89.85 +AXON,Axon Enterprise,436.36 +NRG,NRG Energy,160.11 +CTSH,Cognizant,71.22 +IBKR,Interactive Brokers Group,76.54 +PAYX,Paychex,94.49 +FISV,Fiserv Inc.,62.72 +ADM,Archer Daniels Midland,69.24 +XYZ,Block Inc.,53.87 +FICO,Fair Isaac,1369.86 +DG,Dollar General,147.49 +DOV,Dover Corporation,232.52 +ROL,Rollins Inc.,65.74 +HPE,Hewlett Packard Enterprise,23.62 +RJF,Raymond James Financial,159.60 +TPR,Tapestry Inc.,154.43 +VICI,Vici Properties,29.18 +TDY,Teledyne Technologies,657.92 +XYL,Xylem Inc.,126.71 +CHTR,Charter Communications,241.08 +ARES,Ares Management Corporation,137.90 +ULTA,Ulta Beauty,684.25 +STLD,Steel Dynamics,206.43 +EXR,Extra Space Storage,142.02 +LEN,Lennar,120.76 +IQV,IQVIA,175.80 +IRM,Iron Mountain,99.67 +KHC,Kraft Heinz,24.88 +PPG,PPG Industries,130.53 +HAL,Halliburton,34.84 +ATO,Atmos Energy,175.22 +DTE,DTE Energy,139.17 +TSCO,Tractor Supply,54.53 +EXPE,Expedia Group,235.08 +CFG,Citizens Financial Group,66.87 +AEE,Ameren,106.08 +TPL,Texas Pacific Land Corporation,416.14 +CBOE,Cboe Global Markets,271.99 +ON,ON Semiconductor,70.68 +MTD,Mettler Toledo,1391.33 +STZ,Constellation Brands,163.02 +BIIB,Biogen,191.29 +DVN,Devon Energy,44.78 +FE,FirstEnergy,47.94 +JBL,Jabil,260.92 +NTRS,Northern Trust,147.45 +HUBB,Hubbell Incorporated,513.63 +WTW,Willis Towers Watson,282.86 +WRB,W. R. Berkley Corporation,71.20 +RF,Regions Financial Corporation,30.86 +PHM,PulteGroup,139.04 +CNP,CenterPoint Energy,40.91 +PPL,PPL Corporation,35.97 +DXCM,Dexcom,68.19 +SW,Smurfit WestRock,50.23 +ES,Eversource Energy,69.77 +GIS,General Mills,48.40 +EIX,Edison International,66.94 +IP,International Paper,48.64 +WSM,Williams-Sonoma,214.52 +CINF,Cincinnati Financial,164.11 +LUV,Southwest Airlines,51.66 +AVB,AvalonBay Communities,179.80 +SYF,Synchrony Financial,72.95 +FIS,Fidelity National Information Services,48.73 +KEY,KeyCorp,22.67 +DLTR,Dollar Tree,125.35 +EQR,Equity Residential,65.09 +EXE,Expand Energy,103.09 +DRI,Darden Restaurants,212.58 +FSLR,First Solar,227.60 +DOW,Dow Inc.,33.98 +CPAY,Corpay,346.25 +AWK,American Water Works,123.54 +CHD,Church & Dwight,100.17 +LH,LabCorp,289.08 +VRSK,Verisk Analytics,171.87 +Q,Qnity Electronics,114.06 +CTRA,Coterra,31.48 +STE,Steris,243.09 +EFX,Equifax,197.29 +VLTO,Veralto,95.33 +BG,Bunge Global,121.39 +DGX,Quest Diagnostics,209.53 +CHRW,C.H. Robinson,196.77 +AMCR,Amcor,49.62 +TSN,Tyson Foods,64.52 +L,Loews Corporation,110.15 +CMS,CMS Energy,74.16 +BRO,Brown & Brown,67.07 +LDOS,Leidos,174.83 +PKG,Packaging Corporation of America,244.06 +JBHT,J.B. Hunt,231.62 +OMC,Omnicom Group,69.53 +EXPD,Expeditors International,163.06 +RL,Ralph Lauren Corporation,359.55 +NVR,NVR Inc.,8082.38 +DD,DuPont,51.29 +HUM,Humana,176.34 +NI,NiSource,44.78 +NTAP,NetApp,105.69 +GPC,Genuine Parts Company,149.43 +LULU,Lululemon Athletica,176.88 +ALB,Albemarle Corporation,176.03 +TROW,T. Rowe Price,94.24 +PFG,Principal Financial Group,92.86 +CSGP,CoStar Group,48.09 +GPN,Global Payments,72.67 +SBAC,SBA Communications,190.43 +SNA,Snap-on,383.21 +CNC,Centene Corporation,40.26 +VRSN,Verisign,215.66 +WAT,Waters Corporation,331.52 +IFF,International Flavors & Fragrances,76.65 +BR,Broadridge Financial Solutions,167.88 +WY,Weyerhaeuser,27.09 +INCY,Incyte,99.35 +LII,Lennox International,553.57 +LYB,LyondellBasell,59.29 +SMCI,Supermicro,31.69 +MKC,McCormick & Company,70.30 +ZBH,Zimmer Biomet,95.21 +PTC,PTC Inc.,155.52 +FTV,Fortive,58.93 +VTRS,Viatris,16.02 +EVRG,Evergy,78.94 +BALL,Ball Corporation,67.29 +HPQ,HP Inc.,19.63 +WST,West Pharmaceutical Services,247.87 +PODD,Insulet Corporation,253.09 +APTV,Aptiv,83.64 +CDW,CDW,135.32 +LNT,Alliant Energy,68.17 +TXT,Textron,96.68 +ESS,Essex Property Trust,262.76 +HOLX,Hologic,75.11 +J,Jacobs Solutions,142.70 +INVH,Invitation Homes,27.16 +TKO,TKO Group Holdings,210.88 +NDSN,Nordson Corporation,295.64 +DECK,Deckers Brands,115.15 +PNR,Pentair,99.79 +COO,Cooper Companies (The),82.84 +MAA,Mid-America Apartment Communities,136.74 +FFIV,F5 Inc.,282.22 +MAS,Masco,76.24 +IEX,IDEX Corporation,211.28 +MRNA,Moderna,40.21 +TRMB,Trimble Inc.,65.06 +ALLE,Allegion,179.40 +HII,Huntington Ingalls Industries,392.15 +CLX,Clorox,125.65 +CF,CF Industries,97.23 +GEN,Gen Digital,24.64 +AVY,Avery Dennison,192.77 +KIM,Kimco Realty,21.96 +HAS,Hasbro,105.26 +ERIE,Erie Indemnity,279.84 +TYL,Tyler Technologies,339.51 +UHS,Universal Health Services,231.22 +BEN,Franklin Resources,27.64 +ALGN,Align Technology,197.24 +SOLV,Solventum,81.25 +BBY,Best Buy,66.84 +REG,Regency Centers,76.49 +SWK,Stanley Black & Decker,90.31 +BF.B,Brown–Forman,30.11 +BLDR,Builders FirstSource,125.44 +HST,Host Hotels & Resorts,19.98 +AKAM,Akamai Technologies,95.00 +EG,Everest Group,331.67 +UDR,UDR Inc.,39.84 +TTD,The Trade Desk Inc.,27.19 +HRL,Hormel Foods,23.79 +DPZ,Domino's,385.50 +ZBRA,Zebra Technologies,250.71 +GNRC,Generac,214.94 +FOX,Fox Corporation (Class B),56.02 +FOXA,Fox Corporation (Class A),61.76 +GDDY,GoDaddy,91.44 +PSKY,Paramount Skydance Corp,10.96 +WYNN,Wynn Resorts,115.18 +JKHY,Jack Henry & Associates,165.63 +CPT,Camden Property Trust,111.66 +DOC,Healthpeak Properties,16.95 +SJM,J.M. Smucker Company (The),109.98 +IVZ,Invesco,26.42 +AES,AES Corporation,16.45 +IT,Gartner,160.21 +GL,Globe Life,144.35 +BAX,Baxter International,22.30 +PNW,Pinnacle West,95.57 +RVTY,Revvity,100.89 +AOS,A. O. Smith,80.03 +AIZ,Assurant,216.77 +TAP,Molson Coors Beverage Company,52.98 +NCLH,Norwegian Cruise Line Holdings,22.76 +POOL,Pool Corporation,270.70 +EPAM,EPAM Systems,180.79 +APA,APA Corporation,28.11 +TECH,Bio-Techne,63.46 +MOS,Mosaic Company (The),31.21 +BXP,BXP Inc.,61.61 +DVA,DaVita,144.55 +SWKS,Skyworks Solutions,63.62 +HSIC,Henry Schein,81.21 +CAG,Conagra Brands,19.95 +MGM,MGM Resorts,36.39 +ARE,Alexandria Real Estate Equities,53.92 +FRT,Federal Realty Investment Trust,107.14 +CPB,Campbell Soup Company,29.16 +NWSA,News Corp (Class A),23.30 +CRL,Charles River Laboratories,164.83 +MTCH,Match Group,31.27 +FDS,FactSet,193.29 +LW,Lamb Weston,50.33 +PAYC,Paycom,117.68 +MOH,Molina Healthcare,122.46 +NWS,News Corp (Class B),26.91 diff --git a/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 5a390b4..b0b4c17 100644 --- a/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,13 +1,12 @@ -no\ntnu\gruppe53\Portfolio.class -no\ntnu\gruppe53\PurchaseCalculator.class -no\ntnu\gruppe53\Stock.class -no\ntnu\gruppe53\Purchase.class -no\ntnu\gruppe53\Share.class -no\ntnu\gruppe53\TransactionCalculator.class -no\ntnu\gruppe53\SaleCalculator.class -no\ntnu\gruppe53\App.class -no\ntnu\gruppe53\Exchange.class -no\ntnu\gruppe53\TransactionArchive.class -no\ntnu\gruppe53\Transaction.class -no\ntnu\gruppe53\Player.class -no\ntnu\gruppe53\Sale.class +no/ntnu/gruppe53/controller/ViewController.class +no/ntnu/gruppe53/controller/GameController.class +no/ntnu/gruppe53/service/LanguageManager$LanguageManagerInner.class +no/ntnu/gruppe53/view/FooterBar$1.class +no/ntnu/gruppe53/model/PurchaseFactory.class +no/ntnu/gruppe53/controller/StartViewController.class +no/ntnu/gruppe53/service/LanguageManager.class +no/ntnu/gruppe53/model/SaleFactory.class +no/ntnu/gruppe53/service/LanguageOption.class +no/ntnu/gruppe53/view/EndView.class +no/ntnu/gruppe53/model/TransactionFactory.class +no/ntnu/gruppe53/service/LanguageRegistry.class diff --git a/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index aa43cc4..5fa44b0 100644 --- a/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/millions/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,13 +1,37 @@ -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\App.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Exchange.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Player.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Portfolio.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Purchase.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\PurchaseCalculator.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Sale.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\SaleCalculator.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Share.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Stock.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\Transaction.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\TransactionArchive.java -C:\Users\Roar\Desktop\Github Repoer\Millions\millions\src\main\java\no\ntnu\gruppe53\TransactionCalculator.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/App.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Player.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Share.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java +/home/alodgaard/Programmering/Skole/Prog 2/Millions/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java