public void TestDuplicateUsers() { List <User> duplicateUser = new List <User>() { new User() { Name = "L", Expenses = new decimal[] { 18 } }, new User() { Name = "L", Expenses = new decimal[] { 3, 5 } }, new User() { Name = "L", Expenses = new decimal[] { 2, 2 } }, }; message = GetHttpResponse(JsonConvert.SerializeObject(duplicateUser), HttpMethod.Post); Assert.AreEqual(System.Net.HttpStatusCode.OK, message.StatusCode); ExpenseRepaymentCollection repayment = message.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(0, repayment.Status); }
public void TestOnePennyOff() { List <User> onePennyOff = new List <User>() { new User() { Name = "L", Expenses = new decimal[] { 10 } }, new User() { Name = "C", Expenses = new decimal[] { 5, 5 } }, new User() { Name = "D", Expenses = new decimal[] { 2, 2, 2, 2, 2.01M } }, }; message = GetHttpResponse(JsonConvert.SerializeObject(onePennyOff), HttpMethod.Post); Assert.AreEqual(System.Net.HttpStatusCode.OK, message.StatusCode); ExpenseRepaymentCollection repayment = message.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(0, repayment.Status); }
public void TestFloatingPointValues() { List <User> floating = new List <User>() { new User() { Name = "L", Expenses = new decimal[] { 10.000000000001M } }, new User() { Name = "C", Expenses = new decimal[] { 5.0002000100999M, 5.0000000000009M } }, new User() { Name = "D", Expenses = new decimal[] { 2.0009999999999M, 1.999M, 1.9999999999999M, 2.00000000000001M, 2 } }, }; message = GetHttpResponse(JsonConvert.SerializeObject(floating), HttpMethod.Post); Assert.AreEqual(System.Net.HttpStatusCode.OK, message.StatusCode); ExpenseRepaymentCollection repayment = message.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(0, repayment.Status); }
public void TestAllExpensesZero() { List <User> zero = new List <User>() { new User() { Name = "L", Expenses = new decimal[] { 0 } }, new User() { Name = "C", Expenses = new decimal[] { 0 } }, new User() { Name = "D", Expenses = new decimal[] { 0 } } }; message = GetHttpResponse(JsonConvert.SerializeObject(zero), HttpMethod.Post); Assert.AreEqual(System.Net.HttpStatusCode.OK, message.StatusCode); ExpenseRepaymentCollection repayment = message.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(0, repayment.Status); }
/// <summary> /// Helper method used to verify if a given Expense Repayment Collection /// can reasonably be used to satisfy the given user set /// </summary> /// <param name="repayment"></param> /// <param name="originalData"></param> /// <returns> true if repayment matches original data, false otherwise</returns> private bool VerifyRepaymentCollection(ExpenseRepaymentCollection repayment, List <User> originalData) { var d = originalData.ToDictionary(x => x.Name, y => y.Expenses.Sum()); foreach (var e in repayment.Repayments) { d[e.PayTo] -= e.Amout; d[e.PayFrom] += e.Amout; } var l = d.Select(x => new ExpenseLineItem() { Name = x.Key, ExpenseAmount = x.Value }); decimal average = l.Average(y => y.ExpenseAmount); return(l.All(x => x.EqualWithinOneCent(average))); }
public void TestValidData() { List <User> valid = new List <User>() { new User() { Name = "L", Expenses = new decimal[] { 5.75M, 35M, 12.79M } }, new User() { Name = "C", Expenses = new decimal[] { 12.00M, 15.00M, 23.23M } }, new User() { Name = "D", Expenses = new decimal[] { 10M, 20M, 38.41M, 45M } } }; using (HttpMessageInvoker client = new HttpMessageInvoker(server)) { using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, APIAddress)) { request.Content = new StringContent(JsonConvert.SerializeObject(valid)); request.Content.Headers.Add("content", "application/json"); request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); using (HttpResponseMessage response = client.SendAsync(request, CancellationToken.None).Result) { Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode); ExpenseRepaymentCollection repayment = response.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(1, repayment.Status); List <ExpenseRepayment> repaymentsList = repayment.Repayments.ToList(); Assert.IsTrue(repayment.Repayments.Any(e => e.PayTo == "D" && e.PayFrom == "L" && e.Amout == 18.85M)); Assert.IsTrue(repayment.Repayments.Any(e => e.PayTo == "D" && e.PayFrom == "C" && e.Amout == 22.16M)); } } } }
public void TestOneUserDoesntPay() { List <User> data = new List <User>() { new User() { Name = "A", Expenses = new decimal[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }, new User() { Name = "B", Expenses = new decimal[] { 11, 6, 7, 8, 9, 10 } }, new User() { Name = "C", Expenses = new decimal[] { 1, 8, 9, 10 } }, new User() { Name = "D", Expenses = new decimal[] { 1, 2, 12.55m } }, new User() { Name = "E", Expenses = new decimal[] { 3, 4, 5, 6, 7, 8, 9.8M, 10 } }, new User() { Name = "F", Expenses = new decimal[] { 1, 20 } }, new User() { Name = "G" }, new User() { Name = "H", Expenses = new decimal[] { 7, 8, 9, 10 } }, new User() { Name = "I", Expenses = new decimal[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }, new User() { Name = "J", Expenses = new decimal[] { 1, 2, 3, 4, 5, 8, 9, 10 } }, new User() { Name = "K", Expenses = new decimal[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }, new User() { Name = "L", Expenses = new decimal[] { 1, 2, 3.14M, 6, 7, 8, 9, 10 } }, new User() { Name = "M", Expenses = new decimal[] { 0.01M, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }, new User() { Name = "N", Expenses = new decimal[] { 1, 2, 3, 10 } }, new User() { Name = "O", Expenses = new decimal[] { 1, 22, 3, 4, 5, 6, 7, 8, 9, 10 } }, new User() { Name = "P", Expenses = new decimal[] { 1, 10 } }, new User() { Name = "Q", Expenses = new decimal[] { 1, 24 } } }; message = GetHttpResponse(JsonConvert.SerializeObject(data), HttpMethod.Post); Assert.AreEqual(System.Net.HttpStatusCode.OK, message.StatusCode); ExpenseRepaymentCollection repayment = message.Content.ReadAsAsync <ExpenseRepaymentCollection>().Result; Assert.AreEqual(1, repayment.Status); // Check that nobody is paying themselves Assert.IsTrue(repayment.Repayments.Any(e => e.PayFrom.Equals(e.PayFrom, StringComparison.CurrentCultureIgnoreCase))); Assert.IsTrue(VerifyRepaymentCollection(repayment, data)); }
public ExpenseRepaymentCollection Post([FromBody] IEnumerable <User> users) { if (users == null || !users.Any() || users.All(u => !u.Expenses.Any())) { // empty result passed ThrowResponseException(HttpStatusCode.BadRequest, "Empty list of users entered"); } if (users.SelectMany(u => u.Expenses).Any(exp => exp < 0)) { // one or more expenses has a negative amount ThrowResponseException(HttpStatusCode.BadRequest, "One or more Expense line items have a negative expense amount"); } ExpenseRepaymentCollection result = new ExpenseRepaymentCollection(); // Flatten list into 1 expense line item per name, with their total expenditure var totalExpenses = users.GroupBy(u => u.Name) .Select(x => new ExpenseLineItem() { Name = x.Key, ExpenseAmount = x.SelectMany(y => y.Expenses).Sum() } ).ToList(); decimal totalExpenditure = 0; try { totalExpenditure = totalExpenses.Sum(e => e.ExpenseAmount); } catch (OverflowException) { ThrowResponseException(HttpStatusCode.BadRequest, "Expenses entered are total greater than maximum precision"); } if (totalExpenditure == 0) { return(result); } decimal equalShares = totalExpenditure / totalExpenses.Count; if (totalExpenses.All(e => e.EqualWithinOneCent(equalShares))) { // All users have already paid equally, no repayments need to be done return(result); } //Because we only ever need to look at the front of the line, a stack is more efficient Stack <ExpenseLineItem> paidMoreThanEqual = new Stack <ExpenseLineItem>(totalExpenses.Where(e => e.ExpenseAmount > equalShares)); Stack <ExpenseLineItem> paidLessThanEqual = new Stack <ExpenseLineItem>(totalExpenses.Where(e => e.ExpenseAmount < equalShares)); List <ExpenseRepayment> repayments = new List <ExpenseRepayment>(); while (paidMoreThanEqual.Any() && paidLessThanEqual.Any()) { ExpenseLineItem currentUnderpayer = paidLessThanEqual.Peek(); ExpenseLineItem currentOverpayer = paidMoreThanEqual.Peek(); decimal amountToPay = Math.Min(currentOverpayer.ExpenseAmount - equalShares, equalShares - currentUnderpayer.ExpenseAmount); repayments.Add(new ExpenseRepayment() { PayFrom = currentUnderpayer.Name, PayTo = currentOverpayer.Name, Amout = amountToPay }); currentOverpayer.ExpenseAmount -= amountToPay; currentUnderpayer.ExpenseAmount += amountToPay; if (currentUnderpayer.EqualWithinOneCent(equalShares)) { paidLessThanEqual.Pop(); } if (currentOverpayer.EqualWithinOneCent(equalShares)) { paidMoreThanEqual.Pop(); } } result.Repayments = repayments.ToArray(); result.Status = (byte)(result.Repayments.Any() ? 1 : 0); return(result); }