diff --git a/data/transactions.txt b/data/transactions.txt index e69de29..c1bf64e 100644 --- a/data/transactions.txt +++ b/data/transactions.txt @@ -0,0 +1,3 @@ +a 40.10 +b 40.10 +c 10.00 diff --git a/src/main/java/balancer/logic/command/AddCommand.java b/src/main/java/balancer/logic/command/AddCommand.java index 3100735..1440bf6 100644 --- a/src/main/java/balancer/logic/command/AddCommand.java +++ b/src/main/java/balancer/logic/command/AddCommand.java @@ -1,6 +1,7 @@ package balancer.logic.command; import java.io.IOException; +import java.text.DecimalFormat; import balancer.storage.Storage; @@ -12,7 +13,7 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; private static final String ADD_COMMAND_SUCCESS = "'s transaction has been successfully added!"; private final String name; - private final int amount; + private final double amount; /** * Constructs an {@code AddCommand} with the specified name and amount. @@ -20,16 +21,17 @@ public class AddCommand extends Command { * @param name the name of the individual involved in the transaction. * @param amount the amount of the transaction. */ - public AddCommand(String name, int amount) { + public AddCommand(String name, double amount) { this.name = name; this.amount = amount; } @Override public CommandResult execute(Storage storage) throws IOException { + DecimalFormat df = new DecimalFormat("0.00"); storage.addTransaction(name, amount); storage.save(); - return new CommandResult(String.format("%s%s %s has now contributed $%.2f", - name, ADD_COMMAND_SUCCESS, name, storage.getAmount(name))); + return new CommandResult(String.format("%s%s %s has now contributed $%s", + name, ADD_COMMAND_SUCCESS, name, df.format(storage.getAmount(name)))); } } diff --git a/src/main/java/balancer/logic/command/CalculateCommand.java b/src/main/java/balancer/logic/command/CalculateCommand.java index 3c3b61c..fc6ead0 100644 --- a/src/main/java/balancer/logic/command/CalculateCommand.java +++ b/src/main/java/balancer/logic/command/CalculateCommand.java @@ -1,7 +1,10 @@ package balancer.logic.command; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; import balancer.storage.Storage; import balancer.storage.Transaction; @@ -14,6 +17,7 @@ */ public class CalculateCommand extends Command { public static final String COMMAND_WORD = "calculate"; + public static final String NO_TRANSACTIONS_REPLY = "All good! No transactions required!"; @Override public CommandResult execute(Storage storage) { @@ -21,7 +25,7 @@ public CommandResult execute(Storage storage) { // Remove all people that contributed exactly the average amount and transforms the amounts to their difference // from the average. +ve means then are owed money and -ve means that they owe money. - ArrayList processed = preprocess(transactions); + List processed = preprocess(transactions); // Run the greedy algorithm to get the minimum number of transactions String result = greedy(processed); @@ -38,24 +42,19 @@ public CommandResult execute(Storage storage) { * @param hashmap Taken straight from the storage. Maps names to {@code Transaction}. * @return An {@code ArrayList} of the processed {@code HashMap}. */ - private ArrayList preprocess(HashMap hashmap) { - ArrayList current = new ArrayList<>(hashmap.values()); - float total = 0; + public List preprocess(HashMap hashmap) { + List current = new ArrayList<>(hashmap.values()); + double total = 0; for (Transaction t: current) { total += t.getAmount(); } - float average = total / current.size(); + double average = total / current.size(); // Changing transactions to be difference from average current.replaceAll(x -> x.update(-average)); // Removing all people who have to do nothing (people who are not owed or owe any money) - current.forEach(x -> { - int index = current.indexOf(x); - if (x.isEmpty()) { - current.remove(index); - } - }); + current = current.stream().filter(t -> !t.isEmpty()).collect(Collectors.toList()); return current; } @@ -68,9 +67,12 @@ private ArrayList preprocess(HashMap hashmap) * @return The String representation of the list of transactions that need to occur in the form of * {@code person1 has to pay person2 $X.XX}. */ - private String greedy(ArrayList processed) { + public String greedy(List processed) { + DecimalFormat df = new DecimalFormat("0.00"); StringBuilder sb = new StringBuilder(); TransactionComparator comparator = new TransactionComparator(); + int numberOfTransactions = 0; + while (!processed.isEmpty()) { // Sort the list processed.sort(comparator); @@ -80,9 +82,10 @@ private String greedy(ArrayList processed) { Transaction biggestReceiver = processed.get(processed.size() - 1); // Make the largest possible transaction from biggestGiver to biggestReceiver - float amountToTransfer = Math.min(-biggestGiver.getAmount(), biggestReceiver.getAmount()); - sb.append(String.format("%s has to pay %s $%.2f\n", - biggestGiver.getPerson(), biggestReceiver.getPerson(), amountToTransfer)); + double amountToTransfer = Math.min(-biggestGiver.getAmount(), biggestReceiver.getAmount()); + sb.append(String.format("%s has to pay %s $%s\n", + biggestGiver.getPerson(), biggestReceiver.getPerson(), df.format(amountToTransfer))); + numberOfTransactions++; // Update transactions and transaction list biggestGiver.update(amountToTransfer); @@ -99,6 +102,8 @@ private String greedy(ArrayList processed) { } } - return sb.toString(); + return sb.length() == 0 + ? NO_TRANSACTIONS_REPLY + : sb.append(String.format("\nNumber of transactions: %d", numberOfTransactions)).toString(); } } diff --git a/src/main/java/balancer/logic/parser/AddParser.java b/src/main/java/balancer/logic/parser/AddParser.java index b95b6b8..2ca5134 100644 --- a/src/main/java/balancer/logic/parser/AddParser.java +++ b/src/main/java/balancer/logic/parser/AddParser.java @@ -15,7 +15,7 @@ public AddCommand parse(String userInput) throws ParserException { } String[] arguments = userInput.split(" "); String name = arguments[0]; - int amount = Integer.parseInt(arguments[1]); + double amount = Double.parseDouble(arguments[1]); return new AddCommand(name, amount); } } diff --git a/src/main/java/balancer/storage/Storage.java b/src/main/java/balancer/storage/Storage.java index c6662ce..88b40c6 100644 --- a/src/main/java/balancer/storage/Storage.java +++ b/src/main/java/balancer/storage/Storage.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -34,7 +35,7 @@ public Storage(String dir, String name) { * @param name Name of the person to add or update. * @param amount The initial or update transaction amount. */ - public void addTransaction(String name, int amount) { + public void addTransaction(String name, double amount) { Transaction current = transactions.get(name); if (current == null) { current = new Transaction(name, amount); @@ -45,10 +46,17 @@ public void addTransaction(String name, int amount) { } public HashMap getTransactions() { - return this.transactions; + // Deep copy so that this.transactions cannot be altered + HashMap copy = new HashMap<>(); + for (Map.Entry entry : this.transactions.entrySet()) { + String person = entry.getValue().getPerson(); + double amount = entry.getValue().getAmount(); + copy.put(entry.getKey(), new Transaction(person, amount)); + } + return copy; } - public float getAmount(String name) { + public double getAmount(String name) { return transactions.get(name).getAmount(); } diff --git a/src/main/java/balancer/storage/Transaction.java b/src/main/java/balancer/storage/Transaction.java index 704a5c0..d873bf3 100644 --- a/src/main/java/balancer/storage/Transaction.java +++ b/src/main/java/balancer/storage/Transaction.java @@ -1,11 +1,13 @@ package balancer.storage; +import java.text.DecimalFormat; + /** * Represents a transaction. */ public class Transaction { private String person; - private float amount; + private double amount; /** * Constructs a transaction. @@ -13,7 +15,7 @@ public class Transaction { * @param person Name of the person involved in the transaction. * @param amount Amount of the transaction. */ - public Transaction(String person, float amount) { + public Transaction(String person, double amount) { this.person = person; this.amount = amount; } @@ -24,7 +26,7 @@ public Transaction(String person, float amount) { * @param amount Amount to update. Can be negative. * @return Updated {@code Transaction}. */ - public Transaction update(float amount) { + public Transaction update(double amount) { this.amount += amount; return this; } @@ -33,7 +35,7 @@ public String getPerson() { return person; } - public float getAmount() { + public double getAmount() { return amount; } @@ -43,7 +45,7 @@ public float getAmount() { * @return Ture if amount is 0. */ public boolean isEmpty() { - float epsilon = 0.00001f; // Had to choose an arbitrarily small value due to float equality being bad. + double epsilon = 0.00001; // Had to choose an arbitrarily small value due to double equality being bad. return Math.abs(amount) < epsilon; } @@ -59,14 +61,15 @@ public static Transaction savedStringToTransaction(String savedString) { throw new UnsupportedOperationException(); } else { String newName = split[0]; - float newAmount = Float.parseFloat(split[1]); + double newAmount = Double.parseDouble(split[1]); return new Transaction(newName, newAmount); } } @Override public String toString() { - return String.format("%s %.2f", person, amount); + DecimalFormat df = new DecimalFormat("0.00"); + return String.format("%s %s", person, df.format(amount)); } } diff --git a/src/main/java/balancer/storage/TransactionComparator.java b/src/main/java/balancer/storage/TransactionComparator.java index eb2c117..ca8ea63 100644 --- a/src/main/java/balancer/storage/TransactionComparator.java +++ b/src/main/java/balancer/storage/TransactionComparator.java @@ -8,6 +8,6 @@ public class TransactionComparator implements Comparator { @Override public int compare(Transaction t1, Transaction t2) { - return Float.compare(t1.getAmount(), t2.getAmount()); + return Double.compare(t1.getAmount(), t2.getAmount()); } } diff --git a/src/test/java/balancer/logic/command/CalculateCommandTest.java b/src/test/java/balancer/logic/command/CalculateCommandTest.java new file mode 100644 index 0000000..455ce7d --- /dev/null +++ b/src/test/java/balancer/logic/command/CalculateCommandTest.java @@ -0,0 +1,118 @@ +package balancer.logic.command; + +import balancer.storage.Transaction; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculateCommandTest { + CalculateCommand cc = new CalculateCommand(); + + @Test + public void one_person_pay_two_persons() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 40)); + transactions.put("Bob", new Transaction("Bob", 40)); + transactions.put("Charlie", new Transaction("Charlie", 10)); + List transactionList = cc.preprocess(transactions); + + String expected = "Charlie has to pay Alice $10.00\n" + + "Charlie has to pay Bob $10.00\n" + + "\n" + + "Number of transactions: 2"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + @Test + public void one_person_pay_two_persons_2dp() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 40.105)); + transactions.put("Bob", new Transaction("Bob", 40.105)); + transactions.put("Charlie", new Transaction("Charlie", 10)); + List transactionList = cc.preprocess(transactions); + + String expected = "Charlie has to pay Alice $10.04\n" + + "Charlie has to pay Bob $10.03\n" + + "\n" + + "Number of transactions: 2"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + @Test + public void four_persons_1() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 10)); + transactions.put("Bob", new Transaction("Bob", 20)); + transactions.put("Charlie", new Transaction("Charlie", 0)); + transactions.put("Don", new Transaction("Don", 10)); + List transactionList = cc.preprocess(transactions); + + String expected = "Charlie has to pay Bob $10.00\n" + + "\n" + + "Number of transactions: 1"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + @Test + public void four_persons_2() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 40)); + transactions.put("Bob", new Transaction("Bob", 40)); + transactions.put("Charlie", new Transaction("Charlie", 10)); + transactions.put("Don", new Transaction("Don", 10)); + List transactionList = cc.preprocess(transactions); + + String expected = "Don has to pay Alice $15.00\n" + + "Charlie has to pay Bob $15.00\n" + + "\n" + + "Number of transactions: 2"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + @Test + public void four_persons_with_one_significantly_more() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 200)); + transactions.put("Bob", new Transaction("Bob", 80)); + transactions.put("Charlie", new Transaction("Charlie", 50)); + transactions.put("Don", new Transaction("Don", 20)); + List transactionList = cc.preprocess(transactions); + + String expected = "Don has to pay Alice $67.50\n" + + "Charlie has to pay Alice $37.50\n" + + "Bob has to pay Alice $7.50\n" + + "\n" + + "Number of transactions: 3"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + @Test + public void four_persons_with_two_significantly_more() { + HashMap transactions = new HashMap<>(); + transactions.put("Alice", new Transaction("Alice", 160)); + transactions.put("Bob", new Transaction("Bob", 120)); + transactions.put("Charlie", new Transaction("Charlie", 50)); + transactions.put("Don", new Transaction("Don", 20)); + List transactionList = cc.preprocess(transactions); + + String expected = "Don has to pay Alice $67.50\n" + + "Charlie has to pay Bob $32.50\n" + + "Charlie has to pay Alice $5.00\n" + + "\n" + + "Number of transactions: 3"; + String actual = cc.greedy(transactionList); + assertEquals(expected, actual); + } + + + +} \ No newline at end of file