Beispiel #1
0
        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);
        }
Beispiel #2
0
        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);
        }
Beispiel #3
0
        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);
        }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        /// <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)));
        }
Beispiel #6
0
        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));
                    }
                }
            }
        }
Beispiel #7
0
        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));
        }
Beispiel #8
0
        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);
        }