From 6854bb9744aac2ebd0ea5fd3b9488e3b1f7d136a Mon Sep 17 00:00:00 2001 From: Sameer Pandit Date: Wed, 8 Sep 2021 17:02:48 +0530 Subject: [PATCH] add split impl --- README.md | 5 + pom.xml | 63 ++++++++++ src/main/java/com/sameerpandit/Driver.java | 76 ++++++++++++ src/main/java/com/sameerpandit/Main.java | 12 ++ src/main/java/com/sameerpandit/Splitwise.java | 38 ++++++ .../com/sameerpandit/business/Constants.java | 6 + .../business/EqualSplitStrategy.java | 33 +++++ .../sameerpandit/business/ExactStrategy.java | 17 +++ .../business/PercentSplitStrategy.java | 26 ++++ .../sameerpandit/business/SplitStrategy.java | 10 ++ .../ExpenseConfigurationValidator.java | 4 + .../com/sameerpandit/db/InMemRepository.java | 82 +++++++++++++ .../java/com/sameerpandit/models/Expense.java | 64 ++++++++++ .../models/NoBalanceException.java | 5 + .../com/sameerpandit/models/Transaction.java | 25 ++++ .../java/com/sameerpandit/models/User.java | 45 +++++++ .../service/ExpenseManagerService.java | 46 +++++++ .../sameerpandit/service/LedgerService.java | 113 ++++++++++++++++++ .../com/sameerpandit/service/UserService.java | 28 +++++ 19 files changed, 698 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/sameerpandit/Driver.java create mode 100644 src/main/java/com/sameerpandit/Main.java create mode 100644 src/main/java/com/sameerpandit/Splitwise.java create mode 100644 src/main/java/com/sameerpandit/business/Constants.java create mode 100644 src/main/java/com/sameerpandit/business/EqualSplitStrategy.java create mode 100644 src/main/java/com/sameerpandit/business/ExactStrategy.java create mode 100644 src/main/java/com/sameerpandit/business/PercentSplitStrategy.java create mode 100644 src/main/java/com/sameerpandit/business/SplitStrategy.java create mode 100644 src/main/java/com/sameerpandit/business/validator/ExpenseConfigurationValidator.java create mode 100644 src/main/java/com/sameerpandit/db/InMemRepository.java create mode 100644 src/main/java/com/sameerpandit/models/Expense.java create mode 100644 src/main/java/com/sameerpandit/models/NoBalanceException.java create mode 100644 src/main/java/com/sameerpandit/models/Transaction.java create mode 100644 src/main/java/com/sameerpandit/models/User.java create mode 100644 src/main/java/com/sameerpandit/service/ExpenseManagerService.java create mode 100644 src/main/java/com/sameerpandit/service/LedgerService.java create mode 100644 src/main/java/com/sameerpandit/service/UserService.java diff --git a/README.md b/README.md index 11da620..f420633 100755 --- a/README.md +++ b/README.md @@ -3,3 +3,8 @@ Welcome to the 2nd Mock Machine Coding Round by [workat.tech](http://workat.tech Please visit [this link](https://workat.tech/machine-coding/practice/splitwise-problem-0kp2yneec2q2) to participate. +Compile and package: +mvn clean package + +Run +java -jar target/splitwise-1.0-SNAPSHOT.jar diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c81f999 --- /dev/null +++ b/pom.xml @@ -0,0 +1,63 @@ + + + + mc-prep + com.sameerpandit + 1.0-SNAPSHOT + + 4.0.0 + + splitwise + + 11 + 11 +
com.sameerpandit.Main
+
+ + + + org.glassfish.hk2 + hk2-locator + 3.0.2 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + ${main} + + + + + + + + org.glassfish.hk2 + hk2-inhabitant-generator + 3.0.2 + + + + generate-inhabitants + + + + + + +
diff --git a/src/main/java/com/sameerpandit/Driver.java b/src/main/java/com/sameerpandit/Driver.java new file mode 100644 index 0000000..4397f74 --- /dev/null +++ b/src/main/java/com/sameerpandit/Driver.java @@ -0,0 +1,76 @@ +package com.sameerpandit; + +import com.sameerpandit.db.InMemRepository; + +import org.jvnet.hk2.annotations.Service; + +import jakarta.inject.Inject; + +@Service +public class Driver { + @Inject + Splitwise splitwise; + @Inject + InMemRepository repository; + public void run(){ + System.out.println("Running Driver"); + test1(); + repository.clear(); + test2(); + } + + public void test1(){ + System.out.println("------------------------------------------------------"); + String user1 = splitwise.addUser("User1", "user1@gmail.com", "9986368688"); + String user2 = splitwise.addUser("User2", "user2@gmail.com", "9986368689"); + String user3 = splitwise.addUser("User3", "user3@gmail.com", "9986368682"); + String user4 = splitwise.addUser("User4", "user4@gmail.com", "9986368683"); + splitwise.show(); +// System.out.println("------------------------------------------------------"); + splitwise.show(user1); +// System.out.println("------------------------------------------------------"); + //EXPENSE u1 1000 4 u1 u2 u3 u4 EQUAL + splitwise.addExpense(1000,"EQUAL",user1, new String[]{user1, user2, user3,user4}); + splitwise.show(user4); +// System.out.println("------------------------------------------------------"); + splitwise.show(user1); +// System.out.println("------------------------------------------------------"); + //EXPENSE u1 1250 2 u2 u3 EXACT 370 880 + splitwise.addExpense(1250,"EXACT",user1, new String[]{user1, user2, user3}, new Integer[]{0, 370, 880}); + splitwise.show(); +// System.out.println("------------------------------------------------------"); + //EXPENSE u4 1200 4 u1 u2 u3 u4 PERCENT 40 20 20 20 + splitwise.addExpense(1200,"PERCENT",user4, new String[]{user1, user2, user3, user4}, new Integer[]{40, 20, 20, 20}); + splitwise.show(user1); +// System.out.println("------------------------------------------------------"); + splitwise.show(); +// System.out.println("------------------------------------------------------"); + } + + public void test2(){ + System.out.println("------------------------------------------------------"); + String user1 = splitwise.addUser("User1", "user1@gmail.com", "9986368688"); + String user2 = splitwise.addUser("User2", "user2@gmail.com", "9986368689"); + String user3 = splitwise.addUser("User3", "user3@gmail.com", "9986368682"); + splitwise.show(); + // System.out.println("------------------------------------------------------"); + splitwise.show(user1); + // System.out.println("------------------------------------------------------"); + //EXPENSE u1 1000 4 u1 u2 u3 u4 EQUAL + splitwise.addExpense(1000,"EQUAL",user1, new String[]{user1, user2, user3}); + splitwise.show(user3); + // System.out.println("------------------------------------------------------"); + splitwise.show(user1); + // System.out.println("------------------------------------------------------"); + //EXPENSE u1 1250 2 u2 u3 EXACT 370 880 + splitwise.addExpense(1250,"EXACT",user1, new String[]{user1, user2,user3 }, new Integer[]{0, 370, 880}); + splitwise.show(); + // System.out.println("------------------------------------------------------"); + //EXPENSE u4 1200 4 u1 u2 u3 u4 PERCENT 40 20 20 20 + splitwise.addExpense(1200,"PERCENT",user3, new String[]{user1, user2, user3}, new Integer[]{60, 20, 20}); + splitwise.show(user1); + // System.out.println("------------------------------------------------------"); + splitwise.show(); + // System.out.println("------------------------------------------------------"); + } +} diff --git a/src/main/java/com/sameerpandit/Main.java b/src/main/java/com/sameerpandit/Main.java new file mode 100644 index 0000000..4a79e08 --- /dev/null +++ b/src/main/java/com/sameerpandit/Main.java @@ -0,0 +1,12 @@ +package com.sameerpandit; + +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.utilities.ServiceLocatorUtilities; + +public class Main { + public static void main(String[] args){ + ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator(); + Driver driver = locator.getService(Driver.class); + driver.run(); + } +} diff --git a/src/main/java/com/sameerpandit/Splitwise.java b/src/main/java/com/sameerpandit/Splitwise.java new file mode 100644 index 0000000..06b47dc --- /dev/null +++ b/src/main/java/com/sameerpandit/Splitwise.java @@ -0,0 +1,38 @@ +package com.sameerpandit; + +import com.sameerpandit.service.ExpenseManagerService; +import com.sameerpandit.service.UserService; + +import org.jvnet.hk2.annotations.Service; + +import jakarta.inject.Inject; + +@Service +public class Splitwise { + + @Inject + ExpenseManagerService expenseManagerService; + + @Inject + UserService userService; + + public String addUser(String name, String email, String phone) { + return userService.addUser(name,email,phone); + } + + public void addExpense(Integer amount, String type, String initiatorId, String[] participants){ + expenseManagerService.addExpense(amount,type,initiatorId,participants); + } + + public void addExpense(Integer amount, String type, String initiatorId, String[] participants, Integer[] splits){ + expenseManagerService.addExpense(amount,type,initiatorId,participants,splits); + } + + public void show(){ + expenseManagerService.show(); + } + + public void show(String userId){ + expenseManagerService.show(userId); + } +} diff --git a/src/main/java/com/sameerpandit/business/Constants.java b/src/main/java/com/sameerpandit/business/Constants.java new file mode 100644 index 0000000..aea3ce0 --- /dev/null +++ b/src/main/java/com/sameerpandit/business/Constants.java @@ -0,0 +1,6 @@ +package com.sameerpandit.business; + +public class Constants { + public static Integer PERCENT_SCALE = 100; + public static Integer EXPENSE_SCALING_FACTOR = 100; +} diff --git a/src/main/java/com/sameerpandit/business/EqualSplitStrategy.java b/src/main/java/com/sameerpandit/business/EqualSplitStrategy.java new file mode 100644 index 0000000..2069e42 --- /dev/null +++ b/src/main/java/com/sameerpandit/business/EqualSplitStrategy.java @@ -0,0 +1,33 @@ +package com.sameerpandit.business; + +import com.sameerpandit.models.Expense; + +import org.jvnet.hk2.annotations.Service; + +import jakarta.inject.Named; + +@Service +@Named("EQUAL") +public class EqualSplitStrategy implements SplitStrategy{ + @Override + public Integer[] split(Expense expense) { + int amountUnScaled = expense.getUnscaledAmount(); + int totalParts = (expense.getParticipants().length); + boolean cleanSplit = (amountUnScaled%totalParts == 0); + Integer[] splitAmounts = new Integer[totalParts]; + int start = 0; + if(!cleanSplit){ + while(amountUnScaled!=0){ + int part = amountUnScaled/(totalParts-start); + splitAmounts[start] = part; + amountUnScaled-=part; + start++; + } + }else{ + for(;start> creditorMap = new ConcurrentHashMap<>(); + private ConcurrentHashMap> debitorMap = new ConcurrentHashMap<>(); + private ConcurrentHashMap userMap = new ConcurrentHashMap<>(); + private ConcurrentHashMap> expenseInitiatorMap = new ConcurrentHashMap<>(); + private AtomicInteger userIdNumber = new AtomicInteger(0); + + public void clear(){ + creditorMap = new ConcurrentHashMap<>(); + debitorMap = new ConcurrentHashMap<>(); + userMap = new ConcurrentHashMap<>(); + expenseInitiatorMap = new ConcurrentHashMap<>(); + userIdNumber = new AtomicInteger(0); + } + + public List getCreditTransactionsByUserId(String userId) { + return creditorMap.getOrDefault(userId, new LinkedList<>()); + } + + public List getDebitTransactionsByUserId(String userId) { + return debitorMap.getOrDefault(userId, new LinkedList<>()); + } + + public List getExpensesInitiatedByUserId(String userId) { + return expenseInitiatorMap.get(userId); + } + + public User getUserDetailsById(String userId) { + return userMap.get(userId); + } + + public List getUsers(){ + LinkedList users = new LinkedList<>(); + users.addAll(userMap.values()); + return users; + } + + public String addUser(String name, String email, String phone) { + String userId = "u" + userIdNumber.incrementAndGet(); + userMap.put(userId, new User(userId, name, email, phone)); + return userId; + } + + public Expense addExpense(Integer amount, String type, String initiatorId, String[] participants) { + Expense expense = new Expense(amount, type, initiatorId, participants); + expenseInitiatorMap.computeIfAbsent(initiatorId, k -> new LinkedList<>()).add(expense); + return expense; + } + + public Expense addExpense(Integer amount, String type, String initiatorId, String[] participants, Integer[] splits) { + Expense expense = new Expense(amount, type, initiatorId, participants,splits); + expenseInitiatorMap.computeIfAbsent(initiatorId, k -> new LinkedList<>()).add(expense); + return expense; + } + + + public void addCreditorTransaction(String creditorId, Transaction tx) { + creditorMap.computeIfAbsent(creditorId, k -> new LinkedList<>()).add(tx); + } + + public void addDebitorTransaction(String debitorId, Transaction tx) { + debitorMap.computeIfAbsent(debitorId, k -> new LinkedList<>()).add(tx); + } + +} diff --git a/src/main/java/com/sameerpandit/models/Expense.java b/src/main/java/com/sameerpandit/models/Expense.java new file mode 100644 index 0000000..5a12cb8 --- /dev/null +++ b/src/main/java/com/sameerpandit/models/Expense.java @@ -0,0 +1,64 @@ +package com.sameerpandit.models; + +import com.sameerpandit.business.Constants; + +public class Expense { + private Integer unscaledAmount; + private String type; + private String initiatorId; + private String[] participants; + private Integer[] unscaledSplitAmounts; + private Integer[] splitPercentages; + + + public Expense(Integer amount, String type, String initiatorId, String[] participants) { + this.unscaledAmount = amount*Constants.EXPENSE_SCALING_FACTOR; + this.type = type; + this.initiatorId = initiatorId; + this.participants = participants; + } + + public Integer[] getUnscaledSplitAmounts() { + return unscaledSplitAmounts; + } + + public Integer[] getSplitPercentages() { + return splitPercentages; + } + + public Expense(Integer amount, String type, String initiatorId, String[] participants, Integer[] splits) { + this.unscaledAmount = amount*Constants.EXPENSE_SCALING_FACTOR; + this.type = type; + this.initiatorId = initiatorId; + this.participants = participants; + if("EXACT".equals(type)){ + unscaledSplitAmounts = splits; + for(int i=0;i splitStrategies; + + private SplitStrategy getSplitStrategy(String expenceTye){ + return splitStrategies.named(expenceTye).get(); + } + + public void addExpense(Integer amount, String type, String initiatorId, String[] participants){ + Expense e = repository.addExpense(amount, type, initiatorId, participants); + ledgerService.addTransaction(e,getSplitStrategy(e.getType())); + } + + public void addExpense(Integer amount, String type, String initiatorId, String[] participants, Integer[] splits){ + Expense e = repository.addExpense(amount, type, initiatorId, participants,splits); + ledgerService.addTransaction(e,getSplitStrategy(e.getType())); + } + + public void show(){ + ledgerService.show(); + } + + public void show(String userId){ + ledgerService.show(userId); + } + +} diff --git a/src/main/java/com/sameerpandit/service/LedgerService.java b/src/main/java/com/sameerpandit/service/LedgerService.java new file mode 100644 index 0000000..8b26567 --- /dev/null +++ b/src/main/java/com/sameerpandit/service/LedgerService.java @@ -0,0 +1,113 @@ +package com.sameerpandit.service; + +import com.sameerpandit.business.Constants; +import com.sameerpandit.business.SplitStrategy; +import com.sameerpandit.db.InMemRepository; +import com.sameerpandit.models.Expense; +import com.sameerpandit.models.NoBalanceException; +import com.sameerpandit.models.Transaction; +import com.sameerpandit.models.User; + +import org.jvnet.hk2.annotations.Service; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +import jakarta.inject.Inject; + +@Service +public class LedgerService { + @Inject + InMemRepository repository; + + public void addTransaction(Expense e, SplitStrategy splitStrategy){ + Integer[] splitAmounts = splitStrategy.split(e); + int start = 0; + for(String participant: e.getParticipants()){ + Transaction tx = new Transaction(splitAmounts[start],e.getInitiatorId(), participant); + repository.addCreditorTransaction(e.getInitiatorId(),tx); + repository.addDebitorTransaction(participant,tx); + start++; + } + } + + public void show(){ + HashSet> userPairs = new HashSet<>(); + try{ + for(Map.Entry> entry : getUserBalancesForUsers().entrySet()){ + String userId = entry.getKey(); + for(Map.Entry uEntry: entry.getValue().entrySet()){ + TreeSet pair = new TreeSet<>(); + pair.add(userId);pair.add(uEntry.getKey()); + User u1 = repository.getUserDetailsById(userId); + User u2 = repository.getUserDetailsById(uEntry.getKey()); + if(userId.equals(u2.getUserId()) || userPairs.contains(pair)) + continue; + logExpense(u1.getName(),u2.getName(),uEntry.getValue()); + userPairs.add(pair); + } + } + }catch (NoBalanceException e){ + System.out.println("No Balances"); + } + + } + + public void logExpense(String debitor, String creditor, Integer unscaledAmount){ + if(unscaledAmount<0) + if(unscaledAmount%Constants.EXPENSE_SCALING_FACTOR==0) + System.out.println(debitor + " owes " + creditor + " :" + (-1 * unscaledAmount / Constants.EXPENSE_SCALING_FACTOR)); + else + System.out.println(debitor + " owes " + creditor + " :" + ((float)(-1 * unscaledAmount) / Constants.EXPENSE_SCALING_FACTOR)); + else + if(unscaledAmount%Constants.EXPENSE_SCALING_FACTOR==0) + System.out.println(creditor + " owes " + debitor + " :" + (unscaledAmount / Constants.EXPENSE_SCALING_FACTOR)); + else + System.out.println(creditor + " owes " + debitor + " :" + ((float)unscaledAmount/ Constants.EXPENSE_SCALING_FACTOR)); + } + + public void show(String userId){ + try{ + User user = repository.getUserDetailsById(userId); + for(Map.Entry entry: getUserBalancesForUser(userId).entrySet()){ + User otherUser = repository.getUserDetailsById(entry.getKey()); + if(userId.equals(otherUser.getUserId())) + continue; + logExpense(user.getName(),otherUser.getName(),entry.getValue()); + } + }catch (NoBalanceException e){ + System.out.println("No Balances"); + } + } + + private Map getUserBalancesForUser(String userId) throws NoBalanceException { + List creditorTxs = repository.getCreditTransactionsByUserId(userId); + List debitorTxs = repository.getDebitTransactionsByUserId(userId); + Map balances = new HashMap<>(); + for(Transaction tx: creditorTxs){ + balances.put(tx.getDebitorId(), balances.getOrDefault(tx.getDebitorId(),0)+tx.getUnscaledAmount()); + } + for(Transaction tx: debitorTxs){ + balances.put(tx.getCreditorId(), balances.getOrDefault(tx.getCreditorId(),0)-tx.getUnscaledAmount()); + } + if(balances.isEmpty()) + throw new NoBalanceException(); + return balances; + } + + private Map> getUserBalancesForUsers() throws NoBalanceException { + List users = repository.getUsers(); + Map> userBalances = new HashMap<>(); + for(User user: users){ + String userId = user.getUserId(); + Map balance = getUserBalancesForUser(userId); + userBalances.put(userId,balance); + } + if(userBalances.isEmpty()) + throw new NoBalanceException(); + return userBalances; + } +} diff --git a/src/main/java/com/sameerpandit/service/UserService.java b/src/main/java/com/sameerpandit/service/UserService.java new file mode 100644 index 0000000..d01cdf3 --- /dev/null +++ b/src/main/java/com/sameerpandit/service/UserService.java @@ -0,0 +1,28 @@ +package com.sameerpandit.service; + +import com.sameerpandit.db.InMemRepository; +import com.sameerpandit.models.User; + +import org.jvnet.hk2.annotations.Service; + +import java.util.List; + +import jakarta.inject.Inject; + +@Service +public class UserService { + @Inject + InMemRepository repository; + + public String addUser(String name, String email, String phone) { + return repository.addUser(name,email,phone); + } + + public User getUserDetails(String userId) { + return repository.getUserDetailsById(userId); + } + + public List getUsers(){ + return repository.getUsers(); + } +}