Buy X Pay for Y Algorithm - c#

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)

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;
}

Related

Group and separate list

I have below entity structure
public class Item
{
public EnumType Type { get; set; }
public int Price { get; set; }
}
public enum EnumType
{
A =1,
B=2,
C =3
}
I have a list of items as follow
var items = new List<Item>
{
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C}
};
If the price and type are same, based on type it need to exclude every nth item from the list and then calculate the sum.
i.e type B = 3, Type C = 4
Which means in above sample data, since there are 3 items each in type B once it group by price and type it need to exclude every 3rd item when calculate sum.
So sum for type B will be 5+5+10+10 and sum for type C will be 15+15+15+15
I tried using modular but seems its not the correct direction
I have tried this so far
static int GetFactorByType(EnumType t)
{
switch(t)
{
case EnumType.A:
return 2;
case EnumType.B:
return 3;
case EnumType.C:
return 4;
default:
return 2;
}
}
var grp = items.GroupBy(g => new { g.Type, g.Price }).Select(s => new
{
type= s.Key.Type,
price = s.Key.Price,
count = s.Count()
}).Where(d => d.count % GetFactorByType(d.type) == 0).ToList();
Here's one solve:
//track the type:nth element discard
var dict = new Dictionary<EnumType, int?>();
dict[EnumType.B] = 3;
dict[EnumType.C] = 4;
//groupby turns our list of items into two collections, depending on whether their type is b or c
var x = items.GroupBy(g => new { g.Type })
.Select(g => new //now project a new collection
{
g.Key.Type, //that has the type
SumPriceWithoutNthElement = //and a sum
//the sum is calculated by reducing the list based on index position: in where(v,i), the i is the index of the item.
//We drop every Nth one, N being determined by a dictioary lookup or 2 if the lookup is null
//we only want list items where (index%N != N-1) is true
g.Where((v, i) => (i % (dict[g.Key.Type]??2)) != ((dict[g.Key.Type] ?? 2) - 1))
.Sum(r => r.Price) //sum the price for the remaining
}
).ToList(); //tolist may not be necessary, i just wanted to look at it
It seemed to me like your question words and your example are not aligned. You said (and did in code):
If the price and type are same, based on type it need to exclude every nth item from the list and then calculate the sum. i.e type B = 3, Type C = 4
Which to me means you should group by Type and Price, so B/5 is one list, and B/10 is another list. But you then said:
Which means in above sample data, since there are 3 items each in type B once it group by price and type it need to exclude every 3rd item when calculate sum. So sum for type B will be 5+5+10+10
I couldn't quite understand this. To me there are 3 items in B/5, so B/5 should be a sum of 10 (B/5 + B/5 + excluded). There are 3 items in B/10, again, should be (B/10 + B/10 + excluded) for a total of 20.
The code above does not group by price. It outputs a collection of 2 items, Type=B,SumWithout=30 and Type=C,SumWithout=60. This one groups by price too, it outputs a 3 item collection:
var x = items.GroupBy(g => new { g.Type, g.Price })
.Select(g => new
{
g.Key.Type,
g.Key.Price,
SumPriceWithoutNthElement =
g.Where((v, i) => (i % (dict[g.Key.Type]??2)) != ((dict[g.Key.Type] ?? 2) - 1))
.Sum(r => r.Price) }
).ToList();
The items are Type=B,Price=5,SumWithout=10 and Type=B,Price=10,SumWithout=20 and Type=C,Price=15,SumWithout=60
Maybe you mean group by type&price, remove every 3rd item (from b, 4th item from c etc), then group again by type only and then sum
This means if your type B prices were
1,1,1,1,2,2,2,2,2
^ ^
we would remove one 1 and one 2 (the Ines with arrows under them), then sum for a total of 9. This is different to removing every 3rd for all type b:
1,1,1,1,2,2,2,2,2
^ ^ ^
?
In which case, maybe group by Type/sum again the SumWithout output from my second example
I did consider that there might be a more efficient ways to do this without LINQ.. and it would nearly certainly be easier to understand the code if if were non LINQ - LINQ isn't necessarily a wonderful magic bullet that can kill all ptoblems, and even though it might look like a hammer with which every problem can be beaten, sometimes it's good to avoid
Depending on how you intended the problem to be solved (is price part of the group key or not) building a dictionary and accumulating 0 instead of th price every Nth element might be one way.. The other way, if price is to be part of the key, could be to sum all the prices and then subtract (count/N)*price from the total price
Grouping by a new object, which is always unique, guarantees you that you'll have as many groups as you have items. Try something like this:
var grp = items.GroupBy(g => $"{g.Type}/{g.Price}").Select(s => new
{
type= s.Value.First().Type,
price = s.Value.First().Price,
count = s.Value.Count()
}).Where(d => count % GetFactorByType(d.type) == 0).ToList();
This way, you group by a string composed from the type/price combination, so if the items are equivalent, the strings will be equal.
The $"{g.Type}/{g.Price}"string amounts to "B/5" for your first three item examples, so it's quite readable as well.

Group by with a foreign key on single LINQ

I have 2 related tables and they are
Schedule
SlotId
Description
Amount
and Slot
Id
Name
Example data:
Schedule
slotid description amount
--------------------------------
1 Morning charge 300
1 Late fee 300
1 Earlier bonus 200
1 Half day 150
2 Morning charge 300
2 Late fee 300
2 Earlier bonus 200
2 Half day 150
3 Morning charge 300
3 Late fee 300
3 Earlier bonus 200
3 Half day 150
Final result wanted as a SlotSchedules list, like this:
SlotId
SlotName
List<Schedules>
I need:
fetched list of Schedules
and then find distinct slots in it
and then iterate through each slot and build the model i needed as below
This is what I tried with LINQ:
List<Schedules> schedulesAll = (from n in dbContext.Schedules
select n).ToList();
var slotsdistinct = schedulesAll.Select(x => x.SlotId).Distinct().ToList();
foreach (var slot in slotsdistinct)
{
var scheduledforslot = schedulesAll.Where(x => x.SlotId == slot).Select(x => x).ToList();
foreach (Schedules _schedule in scheduledforslot)
{
//ListModel.Add(new DetailsModel { Name = _schedule.Description, Amount = (_schedule.Amount });
}
}
Any way to make it in single LINQ query?
Try this:
var slotsSchedules = dbContext.Slots.ToList().Select(slot => new {
SlotID = slot.Id,
Name = slot.Name,
SlotSchedules = dbContext.Schedules.ToList()
.Where(schedule = > schedule.SlotId == slot.Id).ToList()});

How to order a collection by a property first and then by another property regardless of tie on first property?

What I am trying to achieve is order the OrderSummary by highest Amount first and then display all other Order(s) in the collection for the given account one after another regardless of the Amount. Expected outcome is in the code snippet..
public class OrderSummary
{
public string FirstName { get; set; }
public decimal Amount { get; set; }
}
public class Worker
{
public List<OrderSummary> Orders { get; set; }
public Worker()
{
Orders = new List<OrderSummary>()
{
new OrderSummary() {FirstName = "James", Amount = 10.00m},
new OrderSummary() {FirstName = "Thomas", Amount = 11.00m},
new OrderSummary() {FirstName = "Leon", Amount = 13.00m},
new OrderSummary() {FirstName = "Lori", Amount = 14.00m},
new OrderSummary() {FirstName = "Thomas", Amount = 16.00m},
new OrderSummary() {FirstName = "Thomas", Amount = 6.00m},
new OrderSummary() {FirstName = "James", Amount = 19.00m}
};
}
//sorted by highest amount first
//then place firstname together regardless of the amount
//Expected Outcome
/*
James 19 -- highest amount followed by all other orders for James regardless of the amount.
* james 10
* thomas 16
* thomas 11
* thomas 6
* lori 14
* leon 13
*/
}
My approach was to get all elements that occurs more than once, then locate at which index the elements are except for the first one, remove it from the index, and add it to first element's index + 1.. Is there a better way to accomplish this?
GroupBy creates groups in the order that they appear in the source collection, so you can first order by amount, then group by name, then "flatten" the groups:
var results = Orders.OrderByDescending(o => o.Amount)
.GroupBy(o => o.FistName) // the groupings will be in order of the largest amount
.SelectMany(g => g); // flatten the groups
Try this:
var list = Orders.OrderByDescending(y => y.Amount).GroupBy(x => x.FirstName).Select(x=>x.Key).ToList();
This is ordering the records descending, then it is grouping them by FirstName, so you will have the one with the highest Amount first, then the others.
var maxAmount = Orders.Max(o => o.Amount);
var maxPos = Orders.IndexOf(maxAmount);
var maxOrder = Orders[maxPos];
Orders.RemoveAt(maxPos);
Orders.Insert(0, maxOrder);
EDIT
I didn't get that "one after another" would mean "grouped by name"...
Sorry, I have an example for you using .Net fiddle
Here it is
Answer To Question
Let me know if this helps
My approach was to get all elements that occurs more than once, then locate at which index the elements are except for the first one, remove it from the index, and add it to first element's index + 1
Here is the LINQ way of doing exactly what are you describing:
var query = from o in Orders
group o by o.FirstName into g
let top = g.Aggregate((a, b) => b.Amount > a.Amount ? b : a)
from o in Enumerable.Repeat(top, 1).Concat(g.Where(o => o != top))
select o;
You start by grouping the Orders by FirstName, then for each group you locate the element with the maximum Amount using Aggregate, put it first in a group and append the others. Finally flatten the result.

Trying to output all information with linq

I've written some code that matches two arrays by the identifiers called seqNum and gets an answer from some math. I'm able to print out the seqNum and the Answer I get but I'm having trouble getting all of the other information that comes along with the seqNum. I want to be able to output something like this:
Name Date `seqNum` Answer...
My code is giving me the answer but then it just spits out the first Name in the file over and over again with different seqNums and Answers.
Here's the code:
private void executeBtn_Click(object sender, EventArgs e)
{
//NET OIL VARIANCE MATHEMATICS
if (netOilRadBtn.Checked)
{
using (var sw = new StreamWriter("testNetOil.csv"))
{
var items = netOil.Zip(seqNum, (oil, seq) => new { Oil = oil, Seq = seq });
var items2 = netOil2.Zip(seqNum2, (oil, seq) => new { Oil = oil, Seq = seq });
sw.WriteLine("Lease Name, Field Name, Reservoir, Operator, County, ST, Majo, Resv Cat, Discount Rate, Net Oil Interest, Net Gas Interest, Working Interest, Gross Wells, Ultimate Oil, Ultimate Gas, Gross Oil, Gross NGL, Gross Gas, Net Oil, Net Gas, Net NGL, Revenue To Int., Oper. Expense, Total Invest., Revenue Oil, Revenue Gas, Operating Profit, Revenue NGL, Disc Net Income, SEQ, Well ID, INC ASN, Life Years, Own Qual, Production Tax, NET OIL VARIANCE");
foreach (var item in items.Join(items2, i => i.Seq, i => i.Seq, (a, b) => new
{SeqID = a.Seq, Answer = this.GetTheAnswer(Convert.ToDouble(a.Oil), Convert.ToDouble(b.Oil))
}))
{
int x = 0;
x.Equals(item.SeqID);
while (x != -1)
{
sw.WriteLine(leaseName[x] + "," + item.SeqID + "," + item.Answer);
x--;
}
So basically I just need to print out the matching name with the correct seqNum and Answer. If anyone has any ideas or comments, that would be greatly appreciated. And my math method is pretty simple if anyone needs to see:
public double GetTheAnswer(double first, double second)
{
double answer = (first - second) / second;
return answer;
}
Well, you should include the data in the anonymous type
foreach (var item in items.Join(items2, i => i.Seq, i => i.Seq, (a, b) => new
{
SeqID = a.Seq,
Answer = this.GetTheAnswer(Convert.ToDouble(a.Oil),
Convert.ToDouble(b.Oil)),
Name = a.Name, // b.Name?
Date = a.Date, // b ?
seqNum = a.seqNum // b ?
}))
What are you trying by (while) looping inside the foreach? You loop through a collection of anonymous types that have no repetitive data in them, item is just a collection of flat data.
So everything after int x = 0; is useless. Just output the data you collect in the anonymous type row by row. Since the header ("Lease Name, Field Name, Reservoir, Operator, ...) is much longer I think there should be much more in the part new { ... }.
You need to change the assignment of x, because you are giving it no value.
int x = Convert.ToInt32(item.SeqID);
Try it and see if you get different results.

C# Merging 2 dictionaries and adding values between them

I am learning C# and needed to merge two dictionaries so I could add values found in both.
The first dictionary is payePlusNssf that holds a value for each key (key represents employee ID). So far I have employees 1,2,3,4 and 5
Dictionary<int, decimal> payePlusNssf = new Dictionary<int, decimal>();
paye.ToList().ForEach(x =>
{
var deductNSSF = x.Value + nssfAmount;
payePlusNssf.Add(x.Key, deductNSSF);
});
The 2nd dictionary is nhifRatesDictionary that holds rates to be added to each value per employee in the first dictionary.
Dictionary<int, IEnumerable<NHIFRates>> nhifRatesDictionary =
new Dictionary<int, IEnumerable<NHIFRates>>();
basicPayDictionary.ToList().ForEach(x =>
{
List<NHIFRates> nhifValueList = new List<NHIFRates>();
// Using Employee basic pay
decimal basicPay = x.Value;
bool foundflag = false;
foreach (var item in nhifBracketList)
{
if (basicPay >= item.Min && basicPay <= item.Max)
{
nhifValueList.Add(new NHIFRates { Rate = item.Rate });
foundflag = true;
break;
}
}
if (!foundflag)
{
nhifValueList.Add(new NHIFRates { Rate = 0m });
}
nhifRatesDictionary.Add(x.Key, nhifValueList);
});
struct NHIFRates
{
public decimal Rate { get; set; }
}
In summary I need this after merging and adding:
Dict 1 Dict 2 Result Dict 3
key value key rate key value
1 500 1 80 1 580
2 1000 2 100 2 1100
3 2000 3 220 3 2220
4 800 4 300 4 1100
5 1000 5 100 5 1100
How do I achieve this? I have looked at past similar problems on this site but have not been very helpful to me.
Not tested but try:
payePlusNssf.ToDictionary(
v => v.Key,
v => v.Value + nhifRatesDictionary[v.Key].Sum(nhifr => nhifr.Rate)
);
This assumes that since the value of nhifRatesDictionary is IEnumreable you want to sum over all the values in the enumerable. This should also work if the IEnumerable is empty. If you know that there is exactly one value for each key then you can use:
payePlusNssf.ToDictionary(
v => v.Key,
v => v.Value + nhifRatesDictionary[v.Key].Single(nhifr => nhifr.Rate)
);
What about a simple for cycle?
Dictionary<int,decimal> d3 = new Dictionary<int,decimal>();
for (int i = 1,i<=payePlusNssf.Count,i++)
{
d3.Add (i,payePlusNssf[i]+((nhifRatesDictionary[i])[0]).Rate);
}
If the ID numbers are not guranteed to be this simple you can use
foreach (var x in payePlusNssf)
{
d3.Add(x.Key,x.Value+ ((nhifRatesDictionary[x.Key])[0]).Rate);
}
Or do it completely differently, do not keep three separate dictionaries that are guaranteed to have the same keys and create an employee class like
class Employee
{
public decimal payePlusNssf;
public decimal nhifRate;
public decimal Sum
{
get { return payePlusNssf + nhifRate ;}
}
}
and have one Dictionary with everything - saves you problems with keeping the dictionaries all updated.

Categories

Resources