diff --git a/.idea/libraries/junit_junit_4_12.xml b/.idea/libraries/junit_junit_4_12.xml
new file mode 100644
index 0000000..d134f71
--- /dev/null
+++ b/.idea/libraries/junit_junit_4_12.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Main.java b/src/Main.java
new file mode 100644
index 0000000..867a4ed
--- /dev/null
+++ b/src/Main.java
@@ -0,0 +1,94 @@
+import model.Identifier;
+import model.InstructionType;
+import model.SplitType;
+import model.TransactionInput;
+import model.ValuePair;
+import service.SplitwiseMainService;
+import service.impl.SplitwiseMainServiceImpl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class Main {
+ public static void main(String args[]) throws IOException {
+ final SplitwiseMainService splitwiseMainService = new SplitwiseMainServiceImpl();
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+ splitwiseMainService.createUser(new Identifier("u3"));
+ splitwiseMainService.createUser(new Identifier("u2"));
+ splitwiseMainService.createUser(new Identifier("u1"));
+ splitwiseMainService.createUser(new Identifier("u4"));
+
+ while(true) {
+ try {
+ final String inputLine = reader.readLine();
+ final String[] input = inputLine.split(" ");
+ if (input[0].equals(InstructionType.EXPENSE.toString())) {
+ final Identifier payer = new Identifier(input[1]);
+ final Double amount = Double.parseDouble(input[2]);
+ final Integer number = Integer.parseInt(input[3]);
+ final List valuePairs = new ArrayList<>();
+ for(int i = 4; i < 4 + number; i++) {
+ valuePairs.add(new ValuePair(new Identifier(input[i]), 0.0));
+ }
+
+ if (!input[4 + number].equals(SplitType.EQUAL.toString())) {
+ for (int i = 5 + number; i < 5 + 2 * number; i++) {
+ valuePairs.get(i - number - 5).setValue(Double.parseDouble(input[i]));
+ }
+ }
+ final TransactionInput transactionInput =
+ TransactionInput.builder()
+ .amount(amount)
+ .payer(payer)
+ .shareParameters(valuePairs)
+ .splitType(SplitType.valueOf(input[4 + number]))
+ .build();
+ splitwiseMainService.addExpense(transactionInput);
+ } else if (input[0].equals(InstructionType.SHOW.toString())) {
+ if (input.length == 1) {
+ formatAndDisplayAll(splitwiseMainService.getBalances());
+ } else {
+ final Identifier user = new Identifier(input[1]);
+ formatAndDisplay(user, splitwiseMainService.getBalanceForUser(user), false);
+ }
+ } else {
+ throw new RuntimeException(String.format("Did not match any command: %s", inputLine));
+ }
+ } catch (final Exception e) {
+ System.out.println("Error: " + e.getMessage());
+ }
+ }
+ }
+
+ private static void formatAndDisplay(final Identifier user, final Map expenseMap, final Boolean allMode) {
+ if(expenseMap.size() == 0) {
+ System.out.println("No balances");
+ }
+ expenseMap.forEach(
+ ((identifier, aDouble) -> {
+ if(aDouble > 0) {
+ System.out.println(identifier.getValue() + " owes " + user.getValue() + " " + Math.abs(aDouble));
+ } else if (!allMode) {
+ System.out.println(user.getValue() + " owes " + identifier.getValue() + " " + Math.abs(aDouble));
+ }
+ })
+ );
+ }
+
+ private static void formatAndDisplayAll(final Map> allExpenseMap) {
+ if (allExpenseMap.keySet().stream()
+ .allMatch(key -> allExpenseMap.get(key).size() == 0)) {
+ System.out.println("No balances");
+ return;
+ }
+
+ allExpenseMap.forEach(
+ (userId, expenseMap) -> {
+ formatAndDisplay(userId, expenseMap, true);
+ });
+ }
+}
diff --git a/src/exception/ShareTypeValidationException.java b/src/exception/ShareTypeValidationException.java
new file mode 100644
index 0000000..56464ae
--- /dev/null
+++ b/src/exception/ShareTypeValidationException.java
@@ -0,0 +1,7 @@
+package exception;
+
+public class ShareTypeValidationException extends IllegalArgumentException{
+ public ShareTypeValidationException(final String msg) {
+ super(msg);
+ }
+}
diff --git a/src/exception/UserAlreadyExistsException.java b/src/exception/UserAlreadyExistsException.java
new file mode 100644
index 0000000..7f5781f
--- /dev/null
+++ b/src/exception/UserAlreadyExistsException.java
@@ -0,0 +1,7 @@
+package exception;
+
+public class UserAlreadyExistsException extends IllegalArgumentException {
+ public UserAlreadyExistsException(String message) {
+ super(message);
+ }
+}
diff --git a/src/model/Identifier.java b/src/model/Identifier.java
new file mode 100644
index 0000000..f90c0cd
--- /dev/null
+++ b/src/model/Identifier.java
@@ -0,0 +1,17 @@
+package model;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.ToString;
+
+@Getter
+@ToString
+@EqualsAndHashCode
+public class Identifier {
+ private final String value;
+
+ public Identifier(@NonNull final String value) {
+ this.value = value;
+ }
+}
diff --git a/src/model/InstructionType.java b/src/model/InstructionType.java
new file mode 100644
index 0000000..62f840f
--- /dev/null
+++ b/src/model/InstructionType.java
@@ -0,0 +1,6 @@
+package model;
+
+public enum InstructionType {
+ SHOW,
+ EXPENSE;
+}
diff --git a/src/model/SplitType.java b/src/model/SplitType.java
new file mode 100644
index 0000000..4e4696a
--- /dev/null
+++ b/src/model/SplitType.java
@@ -0,0 +1,84 @@
+package model;
+
+import exception.ShareTypeValidationException;
+import lombok.NonNull;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public enum SplitType {
+ EQUAL {
+ public Map splitAmount(@NonNull final Double amount, @NonNull final List shareParameters) {
+ final Integer numberOfUsers = shareParameters.size();
+ final Map shareAmounts = new HashMap<>();
+ final Double amountToBeShared = amount / numberOfUsers;
+ shareParameters
+ .forEach(
+ shareParameter -> shareAmounts.put(shareParameter.getKey(), truncateToTwoDecimalPlaces(amountToBeShared)));
+ adjustToTwoDecimalPlaces(shareParameters, amount, shareAmounts);
+ return shareAmounts;
+ }
+ },
+ EXACT {
+ public Map splitAmount(@NonNull final Double amount, @NonNull final List shareParameters) {
+ SplitType.validateTotal(shareParameters, amount);
+ final Map shareAmounts = shareParameters.stream()
+ .collect(Collectors.toMap(ValuePair::getKey, valuePair -> truncateToTwoDecimalPlaces(valuePair.getValue())));
+ adjustToTwoDecimalPlaces(shareParameters, amount, shareAmounts);
+ return shareAmounts;
+ }
+ },
+ PERCENTAGE {
+ public Map splitAmount(@NonNull final Double amount, @NonNull final List shareParameters) {
+ final Map shareAmounts = new HashMap<>();
+ SplitType.validateTotal(shareParameters, 100.0);
+ shareParameters.forEach(
+ valuePair -> shareAmounts.put(valuePair.getKey(), truncateToTwoDecimalPlaces((valuePair.getValue() * amount)/100)));
+ adjustToTwoDecimalPlaces(shareParameters, amount, shareAmounts);
+ return shareAmounts;
+ }
+ },
+ SHARES {
+ public Map splitAmount(@NonNull final Double amount, @NonNull final List shareParameters) {
+ final Map shareAmounts = new HashMap<>();
+ final Double shareSum = shareParameters.stream()
+ .map(ValuePair::getValue)
+ .reduce((a, b) -> a + b)
+ .orElseThrow(() -> new ShareTypeValidationException("Error occurred in processing SHARES split"));
+ shareParameters.forEach(
+ valuePair -> shareAmounts.put(valuePair.getKey(), truncateToTwoDecimalPlaces((valuePair.getValue() * amount)/shareSum)));
+ adjustToTwoDecimalPlaces(shareParameters, amount, shareAmounts);
+ return shareAmounts;
+ }
+ };
+
+ private static void validateTotal(final List valuePairs, final Double amount) {
+ valuePairs.stream()
+ .map(ValuePair::getValue)
+ .reduce((a, b) -> a + b)
+ .filter(sumAmount -> sumAmount.equals(amount))
+ .orElseThrow(() -> new ShareTypeValidationException("Values do not add for split"));
+ }
+
+ private static Double truncateToTwoDecimalPlaces(final Double amount) {
+ return Math.floor(amount * 100)/100;
+ }
+
+ private static void adjustToTwoDecimalPlaces(final List shareParameters,
+ final Double amount,
+ final Map expenses) {
+ final Double totalAmount = expenses.keySet().stream()
+ .map(expenses::get)
+ .reduce((a, b) -> a + b).get();
+
+ if (!totalAmount.equals(amount)) {
+ final Double diff = amount - totalAmount;
+ expenses.put(shareParameters.get(0).getKey(), expenses.get(shareParameters.get(0).getKey()) + diff);
+ }
+ }
+
+ public abstract Map splitAmount(@NonNull final Double amount, @NonNull final List shareParameters);
+
+}
diff --git a/src/model/TransactionInput.java b/src/model/TransactionInput.java
new file mode 100644
index 0000000..2abe670
--- /dev/null
+++ b/src/model/TransactionInput.java
@@ -0,0 +1,34 @@
+package model;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.List;
+
+@Builder(toBuilder = true)
+@Getter
+public class TransactionInput {
+ @NonNull
+ private final Identifier payer;
+
+ @NonNull
+ private final SplitType splitType;
+
+ @NonNull
+ private final List shareParameters;
+
+ @NonNull
+ private final Double amount;
+
+ public static class TransactionInputBuilder {
+ public TransactionInputBuilder amount(Double amount) {
+ final Double truncatedAmount = Math.floor(amount * 100)/100;
+ if(!truncatedAmount.equals(amount)) {
+ throw new IllegalArgumentException("More than two digits specified in input amount");
+ }
+ this.amount = amount;
+ return this;
+ }
+ }
+}
diff --git a/src/model/User.java b/src/model/User.java
new file mode 100644
index 0000000..42adf73
--- /dev/null
+++ b/src/model/User.java
@@ -0,0 +1,21 @@
+package model;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.Map;
+
+@Builder
+@Getter
+public class User {
+ @NonNull
+ private Identifier identifier;
+
+ private String name;
+
+ private String email;
+
+ @NonNull
+ private Map balances;
+}
diff --git a/src/model/ValuePair.java b/src/model/ValuePair.java
new file mode 100644
index 0000000..7f331d7
--- /dev/null
+++ b/src/model/ValuePair.java
@@ -0,0 +1,14 @@
+package model;
+
+import lombok.Data;
+import lombok.NonNull;
+
+@Data
+public class ValuePair {
+ @NonNull
+ private final Identifier key;
+
+ @NonNull
+ private Double value;
+
+}
diff --git a/src/service/SplitwiseMainService.java b/src/service/SplitwiseMainService.java
new file mode 100644
index 0000000..cb74c62
--- /dev/null
+++ b/src/service/SplitwiseMainService.java
@@ -0,0 +1,16 @@
+package service;
+
+import model.Identifier;
+import model.TransactionInput;
+
+import java.util.Map;
+
+public interface SplitwiseMainService {
+ public void createUser(Identifier userId);
+
+ public void addExpense(TransactionInput transaction);
+
+ public Map> getBalances();
+
+ public Map getBalanceForUser(Identifier userId);
+}
diff --git a/src/service/impl/SplitwiseMainServiceImpl.java b/src/service/impl/SplitwiseMainServiceImpl.java
new file mode 100644
index 0000000..1532959
--- /dev/null
+++ b/src/service/impl/SplitwiseMainServiceImpl.java
@@ -0,0 +1,86 @@
+package service.impl;
+
+import exception.UserAlreadyExistsException;
+import lombok.NonNull;
+import model.TransactionInput;
+import model.Identifier;
+import model.User;
+import service.SplitwiseMainService;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class SplitwiseMainServiceImpl implements SplitwiseMainService {
+ private final Map userMap = new HashMap<>();
+
+ @Override
+ public void addExpense(@NonNull final TransactionInput transaction) {
+ final Identifier payer = transaction.getPayer();
+ final Map payerExpenses = getBalanceForUser(payer);
+
+ checkUserExists(payer);
+
+ final Map expenseAmounts = transaction.getSplitType().splitAmount(transaction.getAmount(),
+ transaction.getShareParameters());
+ expenseAmounts.keySet().forEach(this::checkUserExists);
+
+ expenseAmounts.keySet().stream()
+ .filter(userId -> !userId.equals(payer))
+ .forEach(userId -> {
+ addExpenseToMap(payerExpenses, userId, expenseAmounts.get(userId));
+ addExpenseToMap(getBalanceForUser(userId), payer, -expenseAmounts.get(userId));
+ });
+ }
+
+ @Override
+ public Map getBalanceForUser(@NonNull final Identifier userId) {
+ if (!userMap.keySet().contains(userId)) {
+ throw new RuntimeException(String.format("User %s does not exist", userId.getValue()));
+ }
+ return userMap.get(userId).getBalances();
+ }
+
+ private void checkUserExists(final Identifier userId) {
+ if (!userMap.keySet().contains(userId)) {
+ throw new RuntimeException(String.format("User %s does not exist", userId.getValue()));
+ }
+ }
+
+ private void addExpenseToMap(final Map expenseMap,
+ final Identifier userId,
+ final Double amount) {
+ if(expenseMap.keySet().contains(userId)) {
+ final Double updatedAmount = expenseMap.get(userId) + amount;
+ if(updatedAmount == 0.0) {
+ expenseMap.remove(userId);
+ } else {
+ expenseMap.put(userId, expenseMap.get(userId) + amount);
+ }
+ } else {
+ expenseMap.put(userId, amount);
+ }
+ }
+
+ @Override
+ public void createUser(@NonNull final Identifier userId) {
+ if(userMap.keySet().contains(userId)) {
+ throw new UserAlreadyExistsException(String.format("User: %s already exists", userId.getValue()));
+ }
+ final User user =
+ User.builder()
+ .identifier(userId)
+ .balances(new HashMap<>())
+ .build();
+ userMap.put(userId, user);
+ }
+
+ @Override
+ public Map> getBalances() {
+ return userMap.keySet().parallelStream()
+ .map(userMap::get)
+ .collect(Collectors.toMap(User::getIdentifier, User::getBalances));
+ }
+
+}
+
diff --git a/tst/model/TransactionInputTest.java b/tst/model/TransactionInputTest.java
new file mode 100644
index 0000000..be685af
--- /dev/null
+++ b/tst/model/TransactionInputTest.java
@@ -0,0 +1,28 @@
+package model;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+
+public class TransactionInputTest {
+
+ @Test
+ public void testTransactionInput() {
+ TransactionInput.builder()
+ .payer(new Identifier("Dummy"))
+ .shareParameters(new ArrayList<>())
+ .splitType(SplitType.EQUAL)
+ .amount(120.1)
+ .build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testTransactionInput_moreThatTwoDigits() {
+ TransactionInput.builder()
+ .payer(new Identifier("Dummy"))
+ .shareParameters(new ArrayList<>())
+ .splitType(SplitType.EQUAL)
+ .amount(120.001)
+ .build();
+ }
+}
diff --git a/tst/service/SplitwiseMainServiceTest.java b/tst/service/SplitwiseMainServiceTest.java
new file mode 100644
index 0000000..8d611ac
--- /dev/null
+++ b/tst/service/SplitwiseMainServiceTest.java
@@ -0,0 +1,149 @@
+package service;
+
+import model.Identifier;
+import model.SplitType;
+import model.TransactionInput;
+import model.ValuePair;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import service.impl.SplitwiseMainServiceImpl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class SplitwiseMainServiceTest {
+
+ private SplitwiseMainService splitwiseMainService;
+ private Identifier u1;
+ private Identifier u2;
+ private Identifier u3;
+
+ @Before
+ public void setUp() {
+ splitwiseMainService = new SplitwiseMainServiceImpl();
+ u1 = new Identifier("UserId1");
+ u2 = new Identifier("UserId2");
+ u3 = new Identifier("UserId3");
+ splitwiseMainService.createUser(u1);
+ splitwiseMainService.createUser(u2);
+ splitwiseMainService.createUser(u3);
+ }
+
+ @Test
+ public void testMainService_EQUAL() {
+ final List valuePairList = new ArrayList<>();
+ valuePairList.add(new ValuePair(u1, 0.0));
+ valuePairList.add(new ValuePair(u2, 0.0));
+ valuePairList.add(new ValuePair(u3, 0.0));
+
+ final List valuePairList2 = new ArrayList<>();
+ valuePairList2.add(new ValuePair(u1, 0.0));
+ valuePairList2.add(new ValuePair(u2, 0.0));
+ valuePairList2.add(new ValuePair(u3, 0.0));
+
+ final List valuePairList3 = new ArrayList<>();
+ valuePairList3.add(new ValuePair(u1, 0.0));
+ valuePairList3.add(new ValuePair(u2, 0.0));
+ valuePairList3.add(new ValuePair(u3, 0.0));
+
+
+ final TransactionInput transactionInput =
+ TransactionInput.builder()
+ .amount(120.0)
+ .payer(u1)
+ .splitType(SplitType.EQUAL)
+ .shareParameters(valuePairList)
+ .build();
+ splitwiseMainService.addExpense(transactionInput);
+ final Map> allBalances = splitwiseMainService.getBalances();
+ Assert.assertEquals(allBalances.get(u1).get(u3), new Double(40.0));
+ Assert.assertEquals(allBalances.get(u1).get(u2), new Double(40.0));
+ Assert.assertEquals(allBalances.get(u3).get(u1), new Double(-40.0));
+ Assert.assertEquals(allBalances.get(u2).get(u1), new Double(-40.0));
+
+ splitwiseMainService.addExpense(
+ transactionInput
+ .toBuilder()
+ .payer(u2)
+ .shareParameters(valuePairList2)
+ .build());
+ splitwiseMainService.addExpense(
+ transactionInput
+ .toBuilder()
+ .payer(u3)
+ .shareParameters(valuePairList3)
+ .build());
+ final Map> allBalances2 = splitwiseMainService.getBalances();
+ Assert.assertTrue(allBalances2.keySet().stream()
+ .allMatch(
+ key -> allBalances2.get(key).size() == 0));
+ }
+
+ @Test
+ public void testMainService_SHARES() {
+ final List valuePairList = new ArrayList<>();
+ valuePairList.add(new ValuePair(u1, 1.0));
+ valuePairList.add(new ValuePair(u2, 1.0));
+ valuePairList.add(new ValuePair(u3, 2.0));
+
+ final TransactionInput transactionInput =
+ TransactionInput.builder()
+ .amount(100.0)
+ .payer(u1)
+ .splitType(SplitType.SHARES)
+ .shareParameters(valuePairList)
+ .build();
+ splitwiseMainService.addExpense(transactionInput);
+ final Map> allBalances = splitwiseMainService.getBalances();
+ Assert.assertEquals(allBalances.get(u1).get(u3), new Double(50.0));
+ Assert.assertEquals(allBalances.get(u1).get(u2), new Double(25.0));
+ Assert.assertEquals(allBalances.get(u3).get(u1), new Double(-50.0));
+ Assert.assertEquals(allBalances.get(u2).get(u1), new Double(-25.0));
+ }
+
+ @Test
+ public void testMainService_PERCENTAGE() {
+ final List valuePairList = new ArrayList<>();
+ valuePairList.add(new ValuePair(u1, 25.0));
+ valuePairList.add(new ValuePair(u2, 25.0));
+ valuePairList.add(new ValuePair(u3, 50.0));
+
+ final TransactionInput transactionInput =
+ TransactionInput.builder()
+ .amount(100.0)
+ .payer(u1)
+ .splitType(SplitType.PERCENTAGE)
+ .shareParameters(valuePairList)
+ .build();
+ splitwiseMainService.addExpense(transactionInput);
+ final Map> allBalances = splitwiseMainService.getBalances();
+ Assert.assertEquals(allBalances.get(u1).get(u3), new Double(50.0));
+ Assert.assertEquals(allBalances.get(u1).get(u2), new Double(25.0));
+ Assert.assertEquals(allBalances.get(u3).get(u1), new Double(-50.0));
+ Assert.assertEquals(allBalances.get(u2).get(u1), new Double(-25.0));
+ }
+
+ @Test
+ public void testMainService_EXACT() {
+ final List valuePairList = new ArrayList<>();
+ valuePairList.add(new ValuePair(u1, 25.0));
+ valuePairList.add(new ValuePair(u2, 25.0));
+ valuePairList.add(new ValuePair(u3, 50.0));
+
+ final TransactionInput transactionInput =
+ TransactionInput.builder()
+ .amount(100.0)
+ .payer(u1)
+ .splitType(SplitType.EXACT)
+ .shareParameters(valuePairList)
+ .build();
+ splitwiseMainService.addExpense(transactionInput);
+ final Map> allBalances = splitwiseMainService.getBalances();
+ Assert.assertEquals(allBalances.get(u1).get(u3), new Double(50.0));
+ Assert.assertEquals(allBalances.get(u1).get(u2), new Double(25.0));
+ Assert.assertEquals(allBalances.get(u3).get(u1), new Double(-50.0));
+ Assert.assertEquals(allBalances.get(u2).get(u1), new Double(-25.0));
+ }
+}