diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9f7dcb2 Binary files /dev/null and b/.DS_Store differ diff --git a/Splitwise/Client.java b/Splitwise/Client.java new file mode 100644 index 0000000..2124a76 --- /dev/null +++ b/Splitwise/Client.java @@ -0,0 +1,77 @@ +package Designs.Splitwise; + +// https://workat.tech/machine-coding/practice/splitwise-problem-0kp2yneec2q2 + +import Designs.Splitwise.machine.Machine; +import Designs.Splitwise.machine.Splitwise; +import Designs.Splitwise.model.User; +import Designs.Splitwise.modeselector.SplitModeSelector; +import Designs.Splitwise.modeselector.SplitTypes; +import Designs.Splitwise.parser.Parser; +import Designs.Splitwise.splitmodes.EqualSplitMode; +import Designs.Splitwise.splitmodes.ExactSplitMode; +import Designs.Splitwise.splitmodes.PercentSplitMode; + +import java.util.HashMap; +import java.util.Map; + +//You can create a few users in your main method. No need to take it as input. +//There will be 3 types of input: +//SplitTypes in the format: EXPENSE +//Show balances for all: SHOW +//Show balances for a single user: SHOW + +//SHOW +//SHOW u1 +//EXPENSE u1 1000 4 u1 u2 u3 u4 EQUAL +//SHOW u4 +//SHOW u1 +//EXPENSE u1 1250 2 u2 u3 EXACT 370 880 +//SHOW +//EXPENSE u4 1200 4 u1 u2 u3 u4 PERCENT 40 20 20 20 +//SHOW u1 +//SHOW +public class Client { + public static void main(String[] args) { + // onboard users in the machine + User u1 = new User("u1", "jack", "jack@gmail.com", 9289); + User u2 = new User("u2", "Tom", "tom@gmail.com", 9282); + User u3 = new User("u3", "kev", "kev@gmail.com", 9281); + User u4 = new User("u4", "rees", "rees@gmail.com", 9280); + + Map usersMap = new HashMap<>(); + usersMap.put(u1.getUserId(), u1); + usersMap.put(u2.getUserId(), u2); + usersMap.put(u3.getUserId(), u3); + usersMap.put(u4.getUserId(), u4); + + // initialise SplitModeSelector + SplitModeSelector splitModeSelector = new SplitModeSelector( + Map.of(SplitTypes.EQUAL, new EqualSplitMode(), + SplitTypes.EXACT, new ExactSplitMode(), + SplitTypes.PERCENT, new PercentSplitMode() + )); + + // Initialise Machine + Machine machine = new Splitwise(usersMap, splitModeSelector); + + // Commands + String equalExpense = "EXPENSE u1 1000 4 u1 u2 u3 u4 EQUAL"; + String exactExpense = "EXPENSE u1 1250 2 u2 u3 EXACT 370 880"; + String percentExpense = "EXPENSE u4 1200 4 u1 u2 u3 u4 PERCENT 40 20 20 20"; + String show = "SHOW"; + String show_u1 = "SHOW u1"; + String show_u4 = "SHOW u4"; + + // Input to Parser + Parser parser = new Parser(machine); + parser.parse(equalExpense); + parser.parse(show_u4); + parser.parse(show_u1); + parser.parse(exactExpense); + parser.parse(show); + parser.parse(percentExpense); + parser.parse(show_u1); + parser.parse(show); + } +} diff --git a/Splitwise/machine/Machine.java b/Splitwise/machine/Machine.java new file mode 100644 index 0000000..ef422e7 --- /dev/null +++ b/Splitwise/machine/Machine.java @@ -0,0 +1,9 @@ +package Designs.Splitwise.machine; + +public interface Machine { + void showAll(); + + void showBalanceFor(String userId); + + void addExpense(String[] tokens); +} diff --git a/Splitwise/machine/Splitwise.java b/Splitwise/machine/Splitwise.java new file mode 100644 index 0000000..5db87dd --- /dev/null +++ b/Splitwise/machine/Splitwise.java @@ -0,0 +1,43 @@ +package Designs.Splitwise.machine; + +import Designs.Splitwise.model.User; +import Designs.Splitwise.splitmodes.SplitMode; +import Designs.Splitwise.modeselector.SplitModeSelector; +import Designs.Splitwise.modeselector.SplitTypes; + +import java.util.Map; + +public class Splitwise implements Machine { + private Map usersMap; + private SplitModeSelector modeSelector; + + public Splitwise(Map usersMap, SplitModeSelector modeSelector) { + this.usersMap = usersMap; + this.modeSelector = modeSelector; + } + + @Override + public void showAll() { + for (Map.Entry entry : usersMap.entrySet()) { + User user = entry.getValue(); + user.showDebt(); + } + } + + @Override + public void showBalanceFor(String userId) { + User user = usersMap.get(userId); + if (user == null) System.out.println("User not found"); + user.showDebt(); + user.showLoan(); + } + + @Override + public void addExpense(String[] tokens) { + int userLength = Integer.parseInt(tokens[3]); + SplitTypes splitType = SplitTypes.valueOf(tokens[4 + userLength]); + + SplitMode mode = modeSelector.select(splitType); + mode.addExpense(usersMap, tokens); + } +} diff --git a/Splitwise/model/User.java b/Splitwise/model/User.java new file mode 100644 index 0000000..b9b47d4 --- /dev/null +++ b/Splitwise/model/User.java @@ -0,0 +1,88 @@ +package Designs.Splitwise.model; + +import java.util.HashMap; +import java.util.Map; + +public class User { + private final String userId; + private final String name; + private final String email; + private final Integer mobileNumber; + private final Map debtMap; + private final Map loanMap; + + public User(String userId, String name, String email, Integer mobileNumber) { + this.userId = userId; + this.name = name; + this.email = email; + this.mobileNumber = mobileNumber; + this.debtMap = new HashMap<>(); + this.loanMap = new HashMap<>(); + } + + public String getUserId() { + return userId; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User)) return false; + + User user = (User) o; + + if (!userId.equals(user.userId)) return false; + if (!getName().equals(user.getName())) return false; + if (!email.equals(user.email)) return false; + return mobileNumber.equals(user.mobileNumber); + } + + @Override + public int hashCode() { + int result = userId.hashCode(); + result = 31 * result + getName().hashCode(); + result = 31 * result + email.hashCode(); + result = 31 * result + mobileNumber.hashCode(); + return result; + } + + public Double debtFrom(User user) { + return debtMap.getOrDefault(user, 0.0); + } + + public Double loanGivenTo(User user) { + return loanMap.getOrDefault(user, 0.0); + } + + public void addLoanFor(User user, Double balance) { + loanMap.put(user, balance); + } + + public void addDebtFor(User user, Double balance) { + debtMap.put(user, balance); + } + + public void removeDebtFor(User user) { + debtMap.remove(user); + } + + public void removeLoanFor(User borrower) { + loanMap.remove(borrower); + } + + public void showDebt() { + for (Map.Entry entry : debtMap.entrySet()) { + System.out.println(name + " owes " + entry.getKey().name + " : " + Math.abs(entry.getValue())); + } + } + + public void showLoan() { + for (Map.Entry entry : loanMap.entrySet()) { + System.out.println(entry.getKey().name + " owes " + name + " : " + Math.abs(entry.getValue())); + } + } +} diff --git a/Splitwise/modeselector/SplitModeSelector.java b/Splitwise/modeselector/SplitModeSelector.java new file mode 100644 index 0000000..9359eec --- /dev/null +++ b/Splitwise/modeselector/SplitModeSelector.java @@ -0,0 +1,17 @@ +package Designs.Splitwise.modeselector; + +import Designs.Splitwise.splitmodes.SplitMode; + +import java.util.Map; + +public class SplitModeSelector { + private Map map; + + public SplitModeSelector(Map map) { + this.map = map; + } + + public SplitMode select(SplitTypes type) { + return map.get(type); + } +} diff --git a/Splitwise/modeselector/SplitTypes.java b/Splitwise/modeselector/SplitTypes.java new file mode 100644 index 0000000..025a62d --- /dev/null +++ b/Splitwise/modeselector/SplitTypes.java @@ -0,0 +1,5 @@ +package Designs.Splitwise.modeselector; + +public enum SplitTypes { + EQUAL, EXACT, PERCENT +} diff --git a/Splitwise/parser/Parser.java b/Splitwise/parser/Parser.java new file mode 100644 index 0000000..56e8bb3 --- /dev/null +++ b/Splitwise/parser/Parser.java @@ -0,0 +1,24 @@ +package Designs.Splitwise.parser; + +import Designs.Splitwise.machine.Machine; + +public class Parser { + private final Machine machine; + + public Parser(Machine machine) { + this.machine = machine; + } + + public void parse(String cmd) { + String[] tokens = cmd.split(" "); + if (tokens[0].equals("SHOW")) { + if (tokens.length == 1) { + machine.showAll(); + } else if (tokens.length == 2) { + machine.showBalanceFor(tokens[1]); + } + } else if (tokens[0].equals("EXPENSE")) { + machine.addExpense(tokens); + } + } +} diff --git a/Splitwise/splitmodes/EqualSplitMode.java b/Splitwise/splitmodes/EqualSplitMode.java new file mode 100644 index 0000000..80deb32 --- /dev/null +++ b/Splitwise/splitmodes/EqualSplitMode.java @@ -0,0 +1,27 @@ +package Designs.Splitwise.splitmodes; + +import Designs.Splitwise.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class EqualSplitMode extends SplitMode { + + @Override + public void addExpense(Map usersMap, String[] tokens) { + initialise(tokens, usersMap); + + double equalShare = expenseToAdd / users.size(); + + for (User borrower : users) { + if (borrower == lender) continue; + // lender side + updateLenderPortfolio(lender, equalShare, borrower); + + // borrower side + updateBorrowerPortfolio(lender, equalShare, borrower); + } + } + +} diff --git a/Splitwise/splitmodes/ExactSplitMode.java b/Splitwise/splitmodes/ExactSplitMode.java new file mode 100644 index 0000000..11ccb83 --- /dev/null +++ b/Splitwise/splitmodes/ExactSplitMode.java @@ -0,0 +1,41 @@ +package Designs.Splitwise.splitmodes; + +import Designs.Splitwise.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ExactSplitMode extends SplitMode { + + @Override + public void addExpense(Map usersMap, String[] tokens) { + initialise(tokens, usersMap); + + List expenseDistribution = populateExpenseDistribution(tokens); + boolean isValid = validate(expenseToAdd, expenseDistribution); + if (!isValid) { + System.out.println("Invalid input"); + return; + } + + int j = 0; + for (User borrower : users) { + double exactShare = expenseDistribution.get(j++); + if (borrower == lender) continue; + // lender side + updateLenderPortfolio(lender, exactShare, borrower); + + // borrower side + updateBorrowerPortfolio(lender, exactShare, borrower); + } + } + + private boolean validate(double expenseToAdd, List expenseDistribution) { + double sum = 0; + for (Double d : expenseDistribution) { + sum += d; + } + return expenseToAdd == sum; + } +} diff --git a/Splitwise/splitmodes/PercentSplitMode.java b/Splitwise/splitmodes/PercentSplitMode.java new file mode 100644 index 0000000..7dd0da8 --- /dev/null +++ b/Splitwise/splitmodes/PercentSplitMode.java @@ -0,0 +1,43 @@ +package Designs.Splitwise.splitmodes; + +import Designs.Splitwise.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PercentSplitMode extends SplitMode { + + @Override + public void addExpense(Map usersMap, String[] tokens) { + initialise(tokens, usersMap); + + List expenseDistribution = populateExpenseDistribution(tokens); + boolean isValid = validate(expenseDistribution); + if (!isValid) { + System.out.println("Invalid input"); + return; + } + + int j = 0; + for (User borrower : users) { + double percent = expenseDistribution.get(j++); + double exactShare = percent / 100 * expenseToAdd; + if (borrower == lender) continue; + // lender side + updateLenderPortfolio(lender, exactShare, borrower); + + // borrower side + updateBorrowerPortfolio(lender, exactShare, borrower); + } + } + + private boolean validate(List expenseDistribution) { + double sum = 0; + for (Double d : expenseDistribution) { + sum += d; + } + return sum == 100.0; + } + +} diff --git a/Splitwise/splitmodes/SplitMode.java b/Splitwise/splitmodes/SplitMode.java new file mode 100644 index 0000000..0e1ab13 --- /dev/null +++ b/Splitwise/splitmodes/SplitMode.java @@ -0,0 +1,74 @@ +package Designs.Splitwise.splitmodes; + +import Designs.Splitwise.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +// i have specifically created this as abstract class instead of interface, because i wanted to have state in this class +// restricted to specific object, so cant be static, restricted its accessibility to only child classes so protected +// and the state shouldn't be final, as i want the state to be present temporary only. +// So, dont require state to be public,static,final that interface provides. + +// this design is flexible,so in future if any diff mode comes it can accomodate by creating another class by just +// extending this base abstract class +public abstract class SplitMode { + protected User lender; + protected double expenseToAdd; + List users = new ArrayList<>(); + + public abstract void addExpense(Map usersMap, String[] tokens); + + protected void initialise(String[] tokens, Map usersMap) { + lender = usersMap.get(tokens[1]); + expenseToAdd = Double.parseDouble(tokens[2]); + int userLength = Integer.parseInt(tokens[3]); + int i; + for (i = 4; i < 4 + userLength; i++) { + users.add(usersMap.get(tokens[i])); + } + } + + protected void updateLenderPortfolio(User lender, double equalShare, User borrower) { + Double creditAmountForLender = lender.loanGivenTo(borrower); + Double debitAmountForLender = lender.debtFrom(borrower); + double lenderAmount = creditAmountForLender + debitAmountForLender + equalShare; + if (lenderAmount > 0) { + lender.removeDebtFor(borrower); + lender.addLoanFor(borrower, lenderAmount); + } else if (lenderAmount < 0) { + lender.removeLoanFor(borrower); + lender.addDebtFor(borrower, lenderAmount); + } else { + lender.removeLoanFor(borrower); + lender.removeDebtFor(borrower); + } + } + + protected void updateBorrowerPortfolio(User lender, double equalShare, User borrower) { + Double loanAmountBorrower = borrower.loanGivenTo(lender); + Double debtAmountBorrower = borrower.debtFrom(lender); + double borrowerAmount = loanAmountBorrower + debtAmountBorrower - equalShare; + if (borrowerAmount > 0) { + borrower.removeDebtFor(lender); + borrower.addLoanFor(lender, borrowerAmount); + } else if (borrowerAmount < 0) { + borrower.removeLoanFor(lender); + borrower.addDebtFor(lender, borrowerAmount); + } else { + borrower.removeDebtFor(lender); + borrower.removeLoanFor(lender); + } + } + + protected List populateExpenseDistribution(String[] tokens) { + int i = 4 + users.size() + 1; + List expenseDistribution = new ArrayList<>(); + while (i < tokens.length) { + expenseDistribution.add(Double.parseDouble(tokens[i])); + i++; + } + return expenseDistribution; + } +}