public void SuccessfullyBarterGoodWithSufficentBarter() { // Setup Market Change for price. MarketMock.Setup(x => x.ChangeForPrice(JobInputs, 100)) .Returns(JobInputs.Copy()); // Add Storage and ForSale sut.Storage.AddProducts(LifeNeed.Object, 1); sut.ForSale.AddProducts(LifeNeed.Object, 1); // Barter 1 LifeNeed for 1 JobInput. var result = sut.BarterGood(JobInputs, LifeNeed.Object, 1, MarketMock.Object); // check transaction AssertProductAmountIsEqual(result, LifeNeed, 1); AssertProductAmountIsEqual(result, JobInput, -1); // Ensure no change in storage or for sale AssertProductAmountIsEqual(sut.Storage, LifeNeed, 1); AssertProductAmountIsEqual(sut.ForSale, LifeNeed, 1); // Ensure that receipt doesn't go above storage or ForSale Assert.That(sut.Storage.GetProductValue(LifeNeed.Object), Is.GreaterThanOrEqualTo(result.GetProductValue(LifeNeed.Object))); Assert.That(sut.ForSale.GetProductValue(LifeNeed.Object), Is.GreaterThanOrEqualTo(result.GetProductValue(LifeNeed.Object))); }
/// <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); }