// A helper function for avsserting products in the collection are correct. private void AssertProductAmountIsEqual(IProductAmountCollection collection, Mock <IProduct> product, double value) { Assert.That(collection.GetProductValue(product.Object), Is.EqualTo(value)); }
/// <summary> /// Given a set of cash and a desired price, get the amount of the cash /// to meet the price (rounded up for safety) /// </summary> /// <param name="AvailableCash">The amount of cash avaliable.</param> /// <param name="price">The price to meet (roughly)</param> /// <returns>The appropriate cash for the price.</returns> /// <exception cref="ArgumentNullException">If <paramref name="AvailableCash"/> is null.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="price"/> is less than or equal to 0.</exception> public IProductAmountCollection ChangeForPrice(IProductAmountCollection AvailableCash, double price) { // ensure cash is not null. if (AvailableCash is null) { throw new ArgumentNullException(nameof(AvailableCash)); } // ensure that the price is greater than 0 if (price <= 0) // TODO, allow this to savely give change for 0. It's not that hard. { throw new ArgumentOutOfRangeException("Price must be greater than 0."); } // first, check that all available cash can meet the price. var totalCash = AvailableCash.Sum(x => x.Item2 * ProductPrices.GetProductValue(x.Item1)); // If the total cash available is less than the price sought, return a copy of the available cash. if (totalCash < price) { return(AvailableCash.Copy()); } // since we have more than we need, var result = new ProductAmountCollection(); // start from the best and start making change, highest to lowest value. foreach (var coin in AvailableCash.OrderByDescending(x => ProductPrices.GetProductValue(x.Item1))) { // if none of that coin exist if (coin.Item2 == 0) { // add it as zero result.AddProducts(coin.Item1, 0); // and skip to the next loop continue; } // coin type var curr = coin.Item1; // coin value var val = ProductPrices.GetProductValue(curr); // if value is higher then remaining price. if (val > price) { // add it as zero result.AddProducts(curr, 0); // and skip continue; } // coin amount var amt = coin.Item2; // how many whole coins can fit into the price. var count = Math.Floor(price / val); // select cap coins at the available level. count = Math.Min(count, amt); // add to our change result.AddProducts(curr, count); // subtract the value we took out price -= val * count; // then go to the next coin. } // if there is a remainder if (price > 0) { // find the smallest coin available foreach (var coin in AvailableCash.OrderBy(x => ProductPrices.GetProductValue(x.Item1))) { // if we have any available if (result.GetProductValue(coin.Item1) < AvailableCash.GetProductValue(coin.Item1)) { // add one result.AddProducts(coin.Item1, 1); // and move on. break; // by logic, only one should be needed as we guaranteed have // more value in coins then the requested price and the // remainder should be smaller than the smallest coin. } } } // return the change. return(result); }