-1

I have to write "Buy X Pay for Y" algorithm. Request that comes to my endpoint is list of Articles

public class Article
{
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

payFor variable comes from db and is defined by package discount id

Here's my algorithm that I wrote so far

if (purchasedQuantity >= minPurchaseQuantity)
{
    var c = 0;
    foreach (var article in articlesForPackage.OrderByDescending(a => a.UnitPrice))
    {
        for (var i = 1; i <= article.Quantity; i++)
        {
            c++;

            if (c > payFor)
            {
                c = 0;

                result.Add(new Discount
                {
                    Value = article.UnitPrice
                });
            }
        }
    }
}

Unfortunately, this algorithm does not work in some cases.

When the package discount is defined buy 3 and pay for 2 it works, but if buy 3 pay for one doesn't work. Could somebody help me?

This is how the algorithm should work: We have 3 articles 1 Art1 - 20$ 2 Art2 - 30$ 3 Art3 - 40$

If minPurchaseQuantity is 3 and payFor is 2 it means that cost of Art1 should be added to result list (because it is the cheapest one)

If minPurchaseQuantity is 3 and payFor is 1 it means that cost of Art2 and Art1 should be added to result list (Now only the Art2 is adding)

Paul Kertscher
  • 9,416
  • 5
  • 32
  • 57
  • Could you describe what is `"Buy X Pay for Y" algorithm` ? – MBo Mar 01 '19 at 12:36
  • `TotalPrice = Price * (PayFor * Quantity / BuyX + Quantity % BuyX)` - Is this really what you want to implement? – Rafalon Mar 01 '19 at 12:38
  • The pricing is buy 3 and pay for 2. The prices of the 6 things I buy are $1, $2, $3, $4, $5, $6. What should i pay? The most common interpretation (in stores) is that the discount would be $1 and $4. Is that your interpretation? Also, what should I pay if I buy only 5 of them (i.e. not the $6 one)? – mjwills Mar 01 '19 at 12:57
  • Yes it is my interpretation. If you buy only 5 Discount only for $1 should be created – Michał Matuszek Mar 01 '19 at 13:02
  • Cool, so step one is batching. You need to batch your items (price descending) in batches of 3. https://github.com/morelinq/MoreLINQ has a `Batch` which does that. Then, for each batch, skip 2 (the number of ones you pay for). The rest of items in that batch are free. Repeat for each batch. – mjwills Mar 01 '19 at 13:03
  • `If you buy only 5 Discount only for $1 should be created` I am going to assume you made a typo, and meant to say a discount of $3 (if buying items for $1 $2 $3 $4 $5)? – mjwills Mar 01 '19 at 13:05
  • If the rule was buy 5 pay for 3, what should I pay if I buy 4 items of $1 $2 $3 $4? – mjwills Mar 01 '19 at 13:08

1 Answers1

1

Well, the main issue is, that you reset c as soon as it gets larger than payFor. This works as long as minPurchaseQuantity-payFor=1, but in other cases it won't.

While it's not as easy as the solution I presented in my first answer, I think the actual algorithm can be implemented more concisely. The following code first batches the items in groups eligible for discount. For each of the batches it then skips as many as payFor items and calculates the discount from the rest

// first batch the items in batches eligible for discount (e.g. batches of three in take 3, pay for x)
var batchedItems = BatchItemsEligibleForDiscount(items, minPurchaseQuantity);
var discounts = batchedItems.Select(batch => batch.Skip(payFor)) 
                            .SelectMany(batch => batch) // flatten nested IEnumerable to an IEnumerable<Artible>
                            .Select(article => new Discount() { Value = article.UnitPrice });

The BatchItemsEligibleForDiscount gets the batches that are eligible for discount (i.e. have 3 items each if it's "take 3, pay for X". Articles with a Quantity>1 are "exploded", i.e. if the quantity is 3, 3 distinct objects are created.

IEnumerable<IEnumerable<Article>> BatchItemsEligibleForDiscount(items, minPurchaseQuantity)
{
    return items.OrderByDescending(article => article.UnitPrice)
                .Select(article => Enumerable.Range(1, article.Quantity).Select(n => new Article() { Quantity = 1, UnitPrice = article.UnitPrice })) // "explode" articles 
                .SelectMany(item => item) // flatten to an IEnumerable<Article>
                .Select((article, index) => new { article, index })
                .GroupBy(x => x.index / minPurchaseQuantity)
                .Where(group => group.Count() == minPurchaseQuantity) // only take batches elegible for discount
                .Select(group => group.Select(x => x.article));
}

See this fiddle for a demonstration.

OLD ANSWER

Calculating the discount is way easier. You can calculate the number of bundles elegible for discount (if its take 3, pay for 2 and 8 items, you have two whole bundles of 3 items each). By calculating the difference between the items to take and the items to pay and multiplying it with the number of bundles and the price per item, you can calculate the discount

var numberOfDiscountableBundles = article.Quantity / amountOfItemsElegibleForDiscount; 
var discount = numberOfDiscountableBundles * (amountOfItemsElegibleForDiscount - payFor) * article.UnitPrice;

Example: Take 3, pay for 1 with 8 items:

numberOfDiscountableBundles = 8 / 3 = 2 (integer division!)
discount = 2 * (3 - 1) * p = 2 * 2 * p = 4 * p

It's two discounted bundles of three items each (six items). Four of those items are not payed for (only one per bundle), hence the total price is discounted by four times the price of a unit.

You could encapsule this in a method

Discount CalculateDiscountForArticle(Article article, int amountOfItemsElegibleForDiscount, int payFor)
{
    var numberOfDiscountableBundles = article.Quantity / amountOfItemsElegibleForDiscount; 
    var discount = numberOfDiscountableBundles * (amountOfItemsElegibleForDiscount - payFor) * article.UnitPrice;

    return new Discount
               {
                   Value = discount
               };
}

And your original function gets as easy as

var discounts = articlesForPackage.OrderByDescending(a => a.UnitPrice)
                                  .Select(a => CalculateDiscountForArticle(a, amountOfItemsElegibleForDiscount, payFor));

EDIT TO OLD ANSWER

If the discount is granted only once per customer and article, the calculation is a bit different

double discount = 0;
if(article.Quantity >= amountOfItemsElegibleForDiscount)
{
    var discount = (amountOfItemsElegibleForDiscount - payFor) * article.UnitPrice;
}
Paul Kertscher
  • 9,416
  • 5
  • 32
  • 57
  • How do you handle `Buy 3, pay for 1` when client picks `2` or `5` or any `3*n+2` products? – Rafalon Mar 01 '19 at 12:52
  • @Rafalon Depends on your requirements. How do **you need it** to be handled? If the discount is applied for each group of 3 items, my first solution applies. If it's granted only once see my edit. – Paul Kertscher Mar 01 '19 at 12:55
  • Added an example to my answer. – Paul Kertscher Mar 01 '19 at 13:00
  • Sorry I try to understand your answer but I can't I have three articles(Art1 - 1$, Art2 - 2$, Art3 - 3$) and should I pay only for 1(2 should I get free) So in this case 2 Discounts should be created (for 1$ and $2). You answer gives me three discounts with 2 as a Value. What shoiuld I do with those values? – Michał Matuszek Mar 01 '19 at 13:53
  • @MichałMatuszek Seems as if I misunderstood your requirements. I thought that you were talking about buy 3 items (of the same type) and pay just 2. – Paul Kertscher Mar 01 '19 at 14:00
  • @MichałMatuszek I'll try to edit my answer accordingly later. – Paul Kertscher Mar 01 '19 at 14:01
  • @PaulKertscher nope, You can mix them together. So for example: you can buy 1$ x 2 and $2 x 1. In this case discount also should be calculated (for 1$ x 1) – Michał Matuszek Mar 01 '19 at 14:03
  • @PaulKertscher Thanks a lot! – Michał Matuszek Mar 01 '19 at 14:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/189263/discussion-between-paul-kertscher-and-michal-matuszek). – Paul Kertscher Mar 01 '19 at 14:04
  • My comment was precisely more about the fact that we didn't really have OP requirements, and so I was amazed as to how you came with an answer :) – Rafalon Mar 01 '19 at 15:36
  • @Rafalon Well, obviously I *thought* that I did understand OPs requirements enough ;) – Paul Kertscher Mar 04 '19 at 06:10
  • @MichałMatuszek please see my edit. I think now I have reflected your requirements in my code. – Paul Kertscher Mar 04 '19 at 07:05