Sub Totalling Relational Datatable - c#

I have a datatable with relational data structure, I need to sum up the sub nodes to their parent nodes all the way up to the top parent (NULL parent Id)
I have attached 2 images which shows the original table and another with the expected results
Cheers

I've taken an approach that simulates data as they could have been materialized from a database by some ORM, i.e. a class that contains data and a collection of children. Plus some "business logic" to calculate the required numbers. So you can choose a db approach as well as an in-memory approach.
In Linqpad:
void Main()
{
var data = new[]
{
new Record { Id = 1, ParentId = null, Qty = 1, Cost = 0.0m },
new Record { Id = 2, ParentId = 1, Qty = 2, Cost = 0.0m },
new Record { Id = 3, ParentId = 1, Qty = 3, Cost = 0.0m },
new Record { Id = 4, ParentId = 2, Qty = 4, Cost = 0.0m },
new Record { Id = 5, ParentId = 3, Qty = 5, Cost = 0.0m },
new Record { Id = 6, ParentId = 2, Qty = 6, Cost = 1.7m },
new Record { Id = 7, ParentId = 4, Qty = 7, Cost = 1.8m },
new Record { Id = 8, ParentId = 5, Qty = 8, Cost = 1.9m },
new Record { Id = 9, ParentId = 5, Qty = 9, Cost = 2.0m },
}.ToList();
// Mimic ORM's job:
data.ForEach(d => d.ChildRecords =
data.Where(c => c.ParentId == d.Id).ToList());
data.Select(d => new { d.Id, d.Cost, d.TotalCost } ).Dump();
}
class Record
{
public int Id { get; set; }
public int? ParentId { get; set; }
public int Qty { get; set; }
private decimal _cost = 0m;
public decimal Cost
{
get { return this._cost + this.ChildRecords.Sum(cr => cr.TotalCost); }
set { this._cost = value; }
}
public decimal TotalCost
{
get { return this.Qty * this.Cost; }
}
public ICollection<Record> ChildRecords;
}
Result:
Id Cost TotalCost
1 619.2 619.2
2 60.6 121.2
3 166 498
4 12.6 50.4
5 33.2 166
6 1.7 10.2
7 1.8 12.6
8 1.9 15.2
9 2 18
An optimization could be to apply some memoization, i.e. let the Cost property store the result of its getter in a private member variable.

Related

insert item in correct position in a c# list

I have a collection of Products in a list (List<Product> ) where product holds id, name and price.
If I would order the list in a descending way based on price, is there a one liner or extensionmethod that allows me to insert a new product in the correct position of the list?
public class Product
{
public int Id {get; set;}
public string Name {get; set;}
public int Price {get; set;} // assume only whole integers for price
}
public class Main()
{
List<Product> products = new();
products.Add(new Product(id= 1, Name="Product1", Price=10 };
products.Add(new Product(id= 2, Name="Product2", Price=15 };
products.Add(new Product(id= 3, Name="Product3", Price=11 };
products.Add(new Product(id= 4, Name="Product4", Price=20 };
products = products.OrderByDescending(prd => prd.Price).ToList();
var newProduct = new({id = 5, Name="new product", Price = 17})
// Is there an short solution available that allows me to insert a new product with
// price = 17 and that will be inserted between products with price 15 and 20?
// Without repeatedly iterating over the list to find the one lower and the one higher
// than the new price and recalculate the index...
var lastIndex = products.FindLastIndex(x => x.Price >= newProduct.Price);
products.Insert(lastIndex + 1, p5);
}
Edit for Solution: I upvoted Tim Schmelter's answer as the most correct one. It is not a single line, as it requires a custom extension method, but I think a single line solution isn't available. Adding it and do a OrderByDescending() works, and is simple, but then depends on the OrderByDescending() statement for the rest of the code...
You can use a SortedList<TKey, TValue>:
SortedList<int, Product> productList = new();
var p = new Product{ Id = 1, Name = "Product1", Price = 10 };
productList.Add(p.Price, p);
p = new Product { Id = 2, Name = "Product2", Price = 15 };
productList.Add(p.Price, p);
p = new Product { Id = 3, Name = "Product3", Price = 11 };
productList.Add(p.Price, p);
p = new Product { Id = 4, Name = "Product4", Price = 20 };
productList.Add(p.Price, p);
p = new Product { Id = 5, Name = "Product5", Price = 17 };
productList.Add(p.Price, p);
foreach(var x in productList)
Console.WriteLine($"{x.Key} {x.Value.Name}");
outputs:
10 Product1
11 Product3
15 Product2
17 Product5
20 Product4
Edit: Note that it doesn't allow duplicate keys, so like a dictionary. You could solve it by using a SortedList<int, List<Product>>. For example with this extension method:
public static class CollectionExtensions
{
public static void AddItem<TKey, TValue>(this SortedList<TKey, List<TValue>> sortedList, TValue item, Func<TValue, TKey> getKey)
{
TKey key = getKey(item);
if (sortedList.TryGetValue(key, out var list))
{
list.Add(item);
}
else
{
sortedList.Add(key, new List<TValue> { item });
}
}
}
Usage:
SortedList<int, List<Product>> productLists = new();
productLists.AddItem(new Product { Id = 1, Name = "Product1", Price = 10 }, p => p.Price);
productLists.AddItem(new Product { Id = 2, Name = "Product2", Price = 10 }, p => p.Price);
productLists.AddItem(new Product { Id = 3, Name = "Product3", Price = 20 }, p => p.Price);
productLists.AddItem(new Product { Id = 4, Name = "Product4", Price = 20 }, p => p.Price);
productLists.AddItem(new Product { Id = 5, Name = "Product5", Price = 15 }, p => p.Price);
foreach (var x in productLists)
Console.WriteLine($"{x.Key} {string.Join("|", x.Value.Select(p => p.Name))}");
outputs:
10 Product1|Product2
15 Product5
20 Product3|Product4
You could calculate the position of the new element before adding it to the list, and then use List.Insert.

Get the best selling product by using C# LINQ

I have a list of Receipts
IEnumerable<Receipt> Receipts =
new List<Receipt>()
{
new Receipt
{
Id = 1, CustomerId = 1, IsCheckedOut = false, OperationDate = new DateTime(2021, 1, 2),
ReceiptDetails = new List<ReceiptDetail>()
{
new ReceiptDetail { Id = 1, ProductId = 1, UnitPrice = 10, Product = ProductEntities.ElementAt(0), DiscountUnitPrice = 9, Quantity = 2, ReceiptId = 1 },
new ReceiptDetail { Id = 2, ProductId = 2, UnitPrice = 20, Product = ProductEntities.ElementAt(1), DiscountUnitPrice = 19, Quantity = 8, ReceiptId = 1},
new ReceiptDetail { Id = 3, ProductId = 3, UnitPrice = 25, Product = ProductEntities.ElementAt(2), DiscountUnitPrice = 24, Quantity = 1, ReceiptId = 1 },
}
},
new Receipt
{
Id = 2, CustomerId = 2, IsCheckedOut = false, OperationDate = new DateTime(2021, 1, 15),
ReceiptDetails = new List<ReceiptDetail>()
{
new ReceiptDetail { Id = 4, ProductId = 1, UnitPrice = 10, Product = ProductEntities.ElementAt(0), DiscountUnitPrice = 9, Quantity = 10, ReceiptId = 2 },
new ReceiptDetail { Id = 5, ProductId = 3, UnitPrice = 25, Product = ProductEntities.ElementAt(2), DiscountUnitPrice = 24, Quantity = 1, ReceiptId = 2 }
}
},
new Receipt
{
Id = 3, CustomerId = 1, IsCheckedOut = false, OperationDate = new DateTime(2021, 2, 15),
ReceiptDetails = new List<ReceiptDetail>()
{
new ReceiptDetail { Id = 6, ProductId = 1, UnitPrice = 10, Product = ProductEntities.ElementAt(0), DiscountUnitPrice = 9, Quantity = 10, ReceiptId = 3 },
new ReceiptDetail { Id = 7, ProductId = 2, UnitPrice = 25, Product = ProductEntities.ElementAt(1), DiscountUnitPrice = 24, Quantity = 1, ReceiptId = 3 }
}
}
};
I need to get the best selling(by quantity) products based on CustomerId
Here is my function
public IEnumerable<Product> GetMostSoldProductByCustomer(int customerId, int productCount)
{
var a = ReceiptEntities.Where(rd => rd.CustomerId == customerId)..SelectMany(x => x.ReceiptDetails);
var b = a.GroupBy(rd => rd.ProductId);
var c = b.Select(p => p.Select(y => y.Quantity).Sum());
}
I'm stuck on this, I have no idea how to connect b and c.
For better understanding, if customerId = 1 and productCount = 3. The function should return 3 products with Id = 1, 2, 3 accordingly in descending order by quantities
One note! The customer whose id = 1 has two receipts, that's why I'm calculating the sum of Quantity as there are the same products in different receipts
Try the following query:
public IEnumerable<int> GetMostSoldProductByCustomer(int customerId, int productCount)
{
var query =
from re in ReceiptEntities
where re.CustomerId == customerId
from rd in re.ReceiptDetails
group rd by rd.ProductId into g
select new
{
ProductId = g.Key,
TotalQuantity = g.Sum(x => x.Quantity)
} into s
orderby descending s.TotalQuantity
select s.ProductId;
return query;
}

Is there a way to retrieve a SINGLE RECORD randomly in a List using Shuffle()? - C#

Found this Post and it has good solution when shuffling items in List<T>.
But in my case i have a class Person which is defined as this:
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Position { get; set; }
}
This is my implementation and usage:
List<Person> workers = new List<Person>()
{
new Person { Id = 1, Name = "Emp 1", Position = "Cashier"},
new Person { Id = 2, Name = "Emp 2", Position = "Sales Clerk"},
new Person { Id = 3, Name = "Emp 3", Position = "Cashier"},
new Person { Id = 4, Name = "Emp 4", Position = "Sales Clerk"},
new Person { Id = 5, Name = "Emp 5", Position = "Sales Clerk"},
new Person { Id = 6, Name = "Emp 6", Position = "Cashier"},
new Person { Id = 7, Name = "Emp 7", Position = "Sales Clerk"}
};
Now i want to shuffle all records and get 1 Sales Clerk. Here is my code and is working:
var worker = workers.OrderBy(x => Guid.NewGuid()).Where(x => x.Position == "Sales Clerk").First();
// This can yield 1 of this random result (Emp 2, Emp 4, Emp 5 and Emp 7).
Console.WriteLine(worker.Name);
But according to the given Post GUID is not good for randomizing record. And the worst is i cant use Shuffle() and call the Where and First() extensions to get the desired result.
How can i do that with Shuffle() extension?
If the question is how to get it so you can chain Shuffle() with the rest of your Linq operators, the answer is to modify the Shuffle method to return reference to the list shuffled:
public static IEnumerable<T> Shuffle<T>(this IList<T> list)
{
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
int n = list.Count;
while (n > 1)
{
byte[] box = new byte[1];
do provider.GetBytes(box);
while (!(box[0] < n * (Byte.MaxValue / n)));
int k = (box[0] % n);
n--;
T value = list[k];
list[k] = list[n];
list[n] = value;
}
return list;
}
Your code then becomes:
var worker = workers.Shuffle().Where(x => x.Position == "Sales Clerk").First();
Random oRandom = new Random();
var worker = workers[oRandom.Next(0,workers.Count)];

C# compare two Lists and update it's column values

I've got two C# lists (List listA and List listB)
how can I compare these two and if duplicate (of specific columns ex. ID_num and ID_cust) is found then update column "ID_duplicate" which is value of listB's columns ID.
DataSet ds = subMain;
List<string> listA = (from r in ds.Tables[0].AsEnumerable()
Select r.Field<string>("ID_num") +
r.Field<string>("ID_cust")).ToList();
DataSet dsMain = Mains;
List<string> listB = (from r in dsMain.Tables[0].AsEnumerable()
select r.Field<string>("ID_num") +
r.Field<string>("ID_cust")).ToList();
I want that listA will contain new column ID_duplicate with value ID_num from listB.
So that duplicates will be somehow linked with this ID_num.
I will then update that ID_duplicate to database.
Edit:
Added more explanation in comment bellow.
If I understand is a join:
var listA = new List<Row> {
new Row { ID= 1, IdNum = 1, IdCust = 1 },
new Row { ID= 2, IdNum = 1, IdCust = 2 },
new Row { ID= 3, IdNum = 2, IdCust = 1 },
new Row { ID= 4, IdNum = 1, IdCust = 3 },
new Row { ID= 5, IdNum = 3, IdCust = 1 },
new Row { ID= 6, IdNum = 4, IdCust = 1 }
};
var listB = new List<Row> {
new Row { ID= 1, IdNum = 5, IdCust = 1 },
new Row { ID= 5, IdNum = 6, IdCust = 2 },
new Row { ID= 7, IdNum = 2, IdCust = 1 },
new Row { ID= 9, IdNum = 1, IdCust = 3 },
new Row { ID= 11, IdNum = 7, IdCust = 2 }
};
var t = (from a in listA
join b in listB on a.IdCust.ToString() + a.IdNum.ToString()
equals
b.IdCust.ToString() + b.IdNum.ToString()
select new
{
ID = a.ID,
IdUpdate = b.ID
}).ToArray();
foreach (var item in t)
{
Console.WriteLine("ID {0} IdUpdate {1}", item.ID, item.IdUpdate);
}
Here the Row class
class Row
{
public int ID { get; set; }
public int IdNum { get; set; }
public int IdCust { get; set; }
}
Obviusly you can create a calculated column on Row class like this
public string ValueToCompare
{
get
{
return this.IdNum.ToString() + this.IdCust.ToString();
}
}
and use this on join for comparison
Max

Group by using linq (range + count)

var data = new[] {
new { Id = 0, Cat = 1, Price = 2 },
new { Id = 1, Cat = 1, Price = 10 },
new { Id = 2, Cat = 1, Price = 30 },
new { Id = 3, Cat = 2, Price = 50 },
new { Id = 4, Cat = 2, Price = 120 },
new { Id = 5, Cat = 2, Price = 200 },
new { Id = 6, Cat = 2, Price = 1024 },
};
var ranges = new[] { 10, 50, 100, 500 };
Needed output is grouped price count by equal or greater than the range used according categories.
(in one linq statement)
cat range count
-------------------------------------
1 10 2 (In 1. categories there is 2 item that price >= 10(range) [10;30])
2 10 4 (In 2. categories there is 4 item that price >= 10(range) [50;120;200;1024])
2 50 4 ....
2 100 3 ....
2 500 1 (In 2. categories there is 1 item that price >= 500(range) [1024])
Try this:
var data = new[] {
new { Id = 0, Cat = 1, Price = 2 },
new { Id = 1, Cat = 1, Price = 10 },
new { Id = 2, Cat = 1, Price = 30 },
new { Id = 3, Cat = 2, Price = 50 },
new { Id = 4, Cat = 2, Price = 120 },
new { Id = 5, Cat = 2, Price = 200 },
new { Id = 6, Cat = 2, Price = 1024 },
};
var ranges = new[] { 10, 50, 100, 500 };
var result = from r in ranges
from g in data
where g.Price >= r
select new {g.Cat, Price=r};
var groupedData =
from d in result
group d by new{d.Cat, d.Price} into g
select new{Cat=g.Key.Cat, Price=g.Key.Price, TotalCount=g.Count()};
This should work:
var values =
data.SelectMany(x => ranges.Where(y => x.Price >= y)
.Select(y => new { Record = x, Range = y }))
.GroupBy(x => new { Cat = x.Record.Cat, Range = x.Range })
.Select(x => new { Cat = x.Key.Cat, Range = x.Key.Range, Count = x.Count()});
Results:
{ Cat = 1, Range = 10, Count = 2 }
{ Cat = 2, Range = 10, Count = 4 }
{ Cat = 2, Range = 50, Count = 4 }
{ Cat = 2, Range = 100, Count = 3 }
{ Cat = 2, Range = 500, Count = 1 }

Categories

Resources