Calculate total of a column using linq in c# - c#

I have a structure like
ID Description Principal Interest Total
123 Paid 100 150 250
Balance 50 50 100
Total ? ? ?
124 Paid 100 150 250
Balance 50 50 100
Total ? ? ?
Class :
public class APIBillingHistory
{
public List<APIBillingHistoryDetails> BillingHistoryDetails;
}
public class APIBillingHistoryDetails
{
public string BillId;
public string Description;
public Decimal Principal;
public Decimal Interest;
public Decimal Total;
}
I would like to sum value for each column for each id. So in above example, Total for ID 123 for Principal would be 150, Interest 200 likewise. I checked this solution over here How to create Total column and Total row using LINQ but not able to get it.
Code:
responseObj = new APIBillingHistory
{
BillingHistoryDetails = billingHistory.BillingHistoryDetails
.GroupBy(detail => new { detail.BillId, detail.Description})
.Select(group => new APIBillingHistoryDetails
{
BillId = group.Key.BillId,
Description = group.Key.Description,
Principal = group.Sum(t => t.Principal),
Interest = group.Sum(t => t.Interest),
Total = group.Sum(t => t.Principal) + group.Sum(t => t.Interest)
}).Concat(new APIBillingHistoryDetails
{
Description = "Total",
/* Not sure what to write over here */
})
};
Edit:
To clarify what I am trying to achieve:
My source record list does not contain "Total" column & "Total" row. I would like to add it manually.
I Was able to figure out how to Add "Total" column which would contain Sum of values.
Principal & Interest will contain values in decimal format.
ID column is an integer. I would like to Display Total row for each ID
Values for Principal & Interest are being calculated by aggregating based on ID, After that calculation, only Total row should get displayed.
Any Suggestions?

You can't use Concat and reference previous data. You need to use SelectMany:
BillingHistoryDetails = billingHistory.BillingHistoryDetails
.GroupBy(detail => detail.BillId)
.SelectMany(bg => bg.Concat(new[] { new APIBillingHistoryDetails {
BillId = bg.First().BillId,
Description = "Total",
Principal = bg.Sum(b => b.Principal),
Interest = bg.Sum(b => b.Interest),
Total = bg.Sum(b => b.Total)
} }));

Assuming the current data
BillId Description Principal Interest
123 Paid 100 150
123 Balance 50 50
124 Paid 100 150
124 Balance 50 50
Then just group by the identity, creating the total object by summing up the grouped properties setting the description as "Total".
var totalsObj = new APIBillingHistory {
BillingHistoryDetails = billingHistory.BillingHistoryDetails
.GroupBy(detail => detail.BillId)
.Select(g => new APIBillingHistoryDetails {
BillId = g.Key,
Description = "Total",
Principal = g.Sum(detail => detail.Principal),
Interest = g.Sum(detail => detail.Interest),
Total = g.Sum(detail => detail.Principal) + g.Sum(detail => detail.Interest)
}).ToList()
};
and totalsObj.BillingHistoryDetails would have desired out put
After that calculation, only Total row should get displayed.
BillId Description Principal Interest Total
123 Total 150 200 350
124 Total 150 200 350

Related

unable to draw chart of highest sold products

I have a soldproduct table I want to draw the chart of the top 10 highest sold products.
trying it with following code but I don't know what is the exact problem while, it is logically right.
here is the error
System.ArgumentException: 'DbExpressionBinding requires an input expression with a collection ResultType. Parameter name: input'
DateTime start = startDate.Value;
DateTime end = EndDate.Value;
var TopTen = (from q in db.soldProduct
where (q.addDate >= start && q.addDate <= end)
orderby q.Barcode.Count() descending
select new
{
count = q.Barcode.Count(),
q.productName
}).Take(10);
foreach (var item in TopTen)
{
chartCustomer.Series["Customer"].Points.AddXY(item.productName, item.count);
}
ADDED :- I have in each row the item name and the quantity which might be one or more, I want to add that as well to the query but it is getting harder
model of the bill of soldProduct contains the following
public string barcode {get;set;}
public int productId{get;set;}
public string productName{get;set;}
public int quantity{get;set;}
public DateTime addDate{get;set;}
public double amount{get;set}
so the table if it has the following data
productId
barcode
productName
quantity
addDate
amount
1
111
milk
3
3/2/2020
15
1
111
milk
1
3/2/2020
5
2
222
bread
5
3/2/2020
20
3
333
cheese
1
3/2/2020
100
1
111
milk
3
3/2/2020
15
2
222
bread
3
3/2/2020
15
quantity of the of item sold the highest is bread with 8 occurrences it should come first then milk with 7 occurrences and then cheese.
Addition after comments: Apparently every SoldProduct has a property Amount. You don't want to count the number of SoldProducts, but the total Amount of all SoldProducts.
So if SoldProduct1 for product "Milk" has Amount 10 and SoldProduct[22] for product "Milk" has Amount 14, you want as result that of product "Milk" 24 items have been sold (= 10 + 14).
End addition after comment
So you have a table, SoldProducts. Every row in this table represents one Sale of a Product.
table SoldProducts has a.o. at least the following columns
AddDate: when was the sale
ProductName: the Name of the sold product,
Amount: the number of sold products in this sale.
I want ... the top 10 highest sold products
"highest sold products" probably doesn't have to do anything to do with the height of the sold products, but with the total Amount of products sold.
To calculate this, you need to make groups of SoldProducts with the same name. You'll get groups of the soldProducts with name: "Bread" and SoldProducts with name "Milk", etc.
Then you want to sum the total Amount of all soldProducts per group.
So if group "Bread" has 3 SoldProducts with Amounts 10, 7, 5, then you want as result: ["Bread", 22]. 22 is the total number of Breads sold.
You order the result of the grouping items by descending TotalAmount and take the first 10 items.
To group elements based on something common is done by one of the overloads of Enumerable.GroupBy. In this case, I take the overload with parameter resultSelector to count the number of elements in each group.
DateTime beginDate = ...
DateTime endDate = ...
var topTenSoldProductNames = dbContext.SoldProducts
.Where(soldProduct => beginDate <= soldProduct.AddDate
&& endDate >= soldProduct.AddDate)
// make groups with same ProductName
.GroupBy(soldProduct => soldProduct.ProductName,
// parameter resultSelector: take each ProductName,
// and all SoldProducts with this ProductName to make one new
(productName, soldProductsWithThisProductName) => new
{
Name = productName,
TotalAmount = soldProductsWithThisProductName
.Select(soldProduct => soldProduct.Amount)
.Sum(),
})
// order by descending TotalAmount, and take the first 10
.OrderByDescending(groupResult => groupResult.TotalAmount)
.Take(10);
Your query is wrong, you are returning count of chars. And without grouping this query is useless.
var groupingQuery =
from q in db.soldProduct
where (q.addDate >= start && q.addDate <= end)
group q by q.productName into g
select new
{
productName = g.Key,
count = g.Count(),
};
var TopTen = groupingQuery.OrderByDescending(x => x.count).Take(10);

Creating key-value pairs from SQL result that have duplicate data

My problem at the moment is I have results coming back from a SQL query that returns a result like this:
125 Month 10.00 Wholesale
125 Year 20.00 Wholesale
126 Month 20.00 Wholesale
126 Year 30.00 Wholesale
127 Month 40.00 Wholesale
127 Year 50.00 Wholesale
where integer column is the ID of the column.
when the data gets returned to the C# calling code, it is placed into an object followering this structure:
PuctName;
}
I am just having issues with how to create the terms without causing an endless amount of loops.
you can use a Dictionary that way you will have a key value pair. the key is ProductID and the value the list of Terms.
var dictionary = new Dictionary<int, List<Terms>>();
foreach (ProductTermAndPricingDataItem item in productInformationItems)
{
if(dictionary.ContainsKey(item.ProductID))
{
dictionary[item.ProductID].Add(new Terms { Term = item.BillingPeriodName, Price = item.PriceAmount});
}
else
{
dictionary.Add(item.ProductID, new List<Terms>() { new Terms() {Term = item.BillingPeriodName, Price = item.PriceAmount } });
}
}
You can use Linq and GroupBy:
List<ProductPricingGetDataItem> grouped = productInformationItems.GroupBy(
p => p.ProductID,
(key, g) => new ProductPricingGetDataItem() { ProductID = key, Terms = g.Select(x => new Terms(x.BillingPeriodName, x.PriceAmount)).ToList() }).ToList();
In order for that code to work, you need to add a constructor to Terms :
public Terms(string term, decimal price)
{
Term = term;
Price = price;
}
Fiddle with working example : https://dotnetfiddle.net/EE2BpP
For LINQ lovers:
//Your initial data list
var productInformationItems = new List<ProductTermAndPricingDataItem>();
var productPricingGetDataItems = productInformationItems.ToLookup(item => item.ProductID)
.Select(grouping => new ProductPricingGetDataItem
{
ProductID = grouping.Key,
Terms = grouping.Select(item => new Terms
{
Price = item.PriceAmount,
Term = item.BillingPeriodName
}).ToList()
}).ToList();
In your exact case the result is :
Feel free to ask if something is not clear.

Buy X Pay for Y Algorithm

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

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.

Categories

Resources