Efficient way to merge table like datastructure using linq - c#

Allow me to present you with my atrocious logic first:
public void MergeLotDataList(List<SPCMeasureData> sPCMeasureDataList)
{
double standMaxTotal = 0.0;
double standAimTotal = 0.0;
double standMinTotal = 0.0;
List<SPCLotData> lotDataRemovalList = new List<SPCLotData>();
foreach (SPCLotData lotData in sPCLotDataList)
{
//Find if there's any lotDatas with duplicate identify strings
var duplicateLotList = sPCLotDataList.Where(w => w.GetIdentifyString() == lotData.GetIdentifyString()).Select(s=>s);
int duplicateLotCount = duplicateLotList.Count();
if (duplicateLotCount <= 1)
continue;
//Get the standMax,standAim,standMin total for computing average later
//and remove duplicates, leaving only a single unique lotData
foreach (SPCLotData lotData_inner in duplicateLotList)
{
standMaxTotal += lotData_inner.GetStandMax();
standAimTotal += lotData_inner.GetStandAim();
standMinTotal += lotData_inner.GetStandMin());
if (lotData_inner != lotData)
lotDataRemovalList.Add(lotData_inner);
}
//Remove all duplicates
foreach (SPCLotData lotDataToRemove in lotDataRemovalList)
{
sPCLotDataList.Remove(lotDataToRemove);
}
lotDataRemovalList.Clear();
//Set the corresponding standdatas to average
lotData.SetStandData((standMaxTotal / duplicateLotCount),
(standAimTotal / duplicateLotCount),
(standMinTotal / duplicateLotCount);
standMaxTotal = 0.0;
standAimTotal = 0.0;
standMinTotal = 0.0;
}
}
Now that I've ensured that my code makes zero sense to everyone (and of course, doesn't work either because i'm modifying the container inside the foreach loop), let me explain what I'm trying to do.
So I have a datastructure like this:
identifyString standMax standAim standMin
-----------------------------------------
AA 3 4 5
AA 1 2 3
AA 1 2 4
AB 0 5 7
AC 3 4 5
The end result I'm trying to get is this:
identifyString standMax standAim standMin
-----------------------------------------
AA 2.5 2.667 4
AB 0 5 7
AC 3 4 5
Notice how the duplicate rows (with same identifyString) have been removed, and the uniquely remaining row have the values (standMax,aim,min) is updated as their average.
What's the most elegant way of achieving this?

You can use LINQ Enumerable.ToLookup and Enumerable.Average extention methods
Here is the what I mean:
var perIdentStrLookup = sPCMeasureDataList.ToLookup(k => k.GetIdentifyString());
foreach(var lk in perIdentStrLookup)
{
Console.WriteLine("identifyString={0}; standMax={1}; standAim={2}; standMin={1}",
lk.Key,//identifyString
lk.Average(l=>GetStandMax()),
lk.Average(l=>GetStandAim()),
lk.Average(l=>GetStandMin()),
)
}
or in case if you want unique list
var uniqueList = sPCMeasureDataList
.ToLookup(k => k.GetIdentifyString())
.Select(lk => new SPCLotData
{
IdentifyString = lk.Key,
StandMax = lk.Average(l=>GetStandMax()),
StandAim = lk.Average(l=>GetStandAim()),
StandMin = lk.Average(l=>GetStandMin())
})
.ToList()

You can use LINQ GroupBy:
var result = sPCLotDataList.GroupBy(x => x.identifyString)
.Select(g => new SPCLotData(){
identifyString = g.Key,
standMax = g.Average(x => x.standMax),
standAim = g.Average(x => x.standAim),
standMin = g.Average(x => x.standMin)
});

I assume sPCLotDataList is where you get the data?
In which case you could:
var result = from x in sPCLotDataList
group x by x.identifyString into grp
select new { identifyString = grp.key
standMax = grp.Average(c => c.standMax)
standAim = grp.Average(c => c.standAim)
standMin= grp.Average(c => c.standMin)
}

Related

Listing after implementing ranking skipping numbers

I am trying to achieve ranking functionality as below:
Name Points rank
ram 9 1
kamal 9 1
preet 8 2
lucky 7 3
kishan 6.5 4
devansh 6 5
neha 6 5
I have used below code to achieve this:
finalResult = finalResult.OrderByDescending(i => i.points).ThenBy(i => i.academy).ToList();
finalResult = finalResult.AsEnumerable() // Client-side from here on
.Select((player, index) => new RankingEntity()
{
competitorid = player.competitorid,
firstname = player.firstname,
lastname = player.lastname,
academy = player.academy,
points = player.points,
place = player.place,
eventId = player.eventId,
eventname = player.eventname,
categoryname = player.categoryname,
Rank = index + 1
}).ToList();
var t = (from i in finalResult
let rank = finalResult.First(x => x.points == i.points)
select new
{
Col1 = i,
Rank = rank.Rank
}).ToList();
List<RankingEntity> ttt = new List<RankingEntity>();
foreach (var item in t)
{
var a = item.Col1;
var row = new RankingEntity();
row.competitorid = a.competitorid;
row.firstname = a.firstname;
row.lastname = a.lastname;
row.academy = a.academy;
row.points = a.points;
row.place = a.place;
row.eventId = a.eventId;
row.eventname = a.eventname;
row.categoryname = a.categoryname;
row.Rank = item.Rank;
ttt.Add(row);
}
And i am getting result like below:
Please help what i am doing wrong.
What you are trying to achieve is a ranking of a "group" so group the results by the points and then order the groups. For each item in the group give the same rank.
finalResult.GroupBy(item => item.Points) // Group by points
.OrderDescendingBy(g => g.Key) // Order the groups
.Select((g, index) => new { Data = g, GroupRank = index + 1}) // Rank each group
.SelectMany(g => g.Data.Select(item => new RankingEntity
{
/* properties of each item */
Rank = g.GroupIndex
}); // Flatten groups and set for each item the group's ranking
The problem in your method is that you give the ranking for individual items and not the group. Then when you retrieve the rank for the group (from i in finalResult let rank = finalResult.First(x => x.points == i.points)...) you actually set for each item in the group the ranking of one of the elements in it. Therefore, if you first got the last item of the group - that will be the Rank value of each item in it.
Also notice that in the first line of your code you use ToList. Therefore there is not need to use AsEnumerable in the line under it - it is already a materialized in memory collection.

Grouping data between ranges using LINQ in C#

I have made a following code to create me a range between two numbers, and data is separated in 7 columns:
private List<double> GetRangeForElements(double minPrice, double maxPrice)
{
double determineRange = Math.Round(maxPrice / 7.00d, 3);
var ranges = new List<double>();
ranges.Insert(0, Math.Round(minPrice, 3));
ranges.Insert(1, determineRange);
for (int i = 2; i < 8; i++)
{
ranges.Insert(i, Math.Round(determineRange * i, 3));
}
return ranges;
}
Now I have list of ranges when I call the method:
var ranges = GetRangeForElements(1,1500);
On the other side now I have the data (a list) that contains following data (properties):
public double TransactionPrice {get;set;}
public int SaleAmount {get;set;}
Input data would be:
Transaction price Sale amount
114.5 4
331.5 6
169.59 8
695.99 14
1222.56 5
Generated range for between 1 and 1500 is:
1
214.28
428.57
642.85
857.14
1071.43
1285.71
1500.00
And the desired output would be:
Range Sales
(1 - 214.28) 12
(214.28 - 428.57) 6
(428.57 - 642.85) 0
(642.85 - 857.14) 14
(857.14 - 1071.43) 0
(1071.43 - 1285.71) 5
(1285.71 - 1500) 0
I've tried something like this:
var priceGroups = _groupedItems.GroupBy(x => ranges.FirstOrDefault(r => r > x.TransactionPrice))
.Select(g => new { Price = g.Key, Sales = g.Sum(x=>x.Sales) })
.ToList();
But this doesn't gives me what I want, the results I receive are completely messed up (I was able to verify the data and results manually)...
Can someone help me out?
P.S. guys, the ranges that have no sales should simply have value set to 0...
#blinkenknight here's a pic of what I'm saying, min price is = 2.45 , max price = 2.45
and the output of the 2nd method you posted is:
Since GetRangeForElements returns a List<double>, you cannot group by it. However, you can group by range index, and then use that index to get the range back:
var rangePairs = ranges.Select((r,i) => new {Range = r, Index = i}).ToList();
var priceGroups = _groupedItems
.GroupBy(x => rangePairs.FirstOrDefault(r => r.Range >= x.TransactionPrice)?.Index ?? -1)
.Select(g => new { Price = g.Key >= 0 ? rangePairs[g.Key].Range : g.Max(x => x.TransactionPrice), Sales = g.Sum(x=>x.Sales) })
.ToList();
Assuming that _groupedItems is a list, you could also start with ranges, and produce the results directly:
var priceGroups = ranges.Select(r => new {
Price = r
, Sales = _groupedItems.Where(x=>ranges.FirstOrDefault(y=>y >= x.TransactionPrice) == r).Sum(x => x.Sales)
});
Note: Good chances are, your GetRangeForElements has an error: it assumes that minPrice is relatively small in comparison to maxPrice / 7.00d. To see this problem, consider what would happen if you pass minPrice=630 and maxPrice=700: you will get 630, 100, 200, 300, ... instead of 630, 640, 650, .... To fix this problem, compute (maxPrice - minPrice) / 7.00d and use it as a step starting at minPrice:
private List<double> GetRangeForElements(double minPrice, double maxPrice) {
double step = (maxPrice - minPrice) / 7.0;
return Enumerable.Range(0, 8).Select(i => minPrice + i*step).ToList();
}

Convert array of doubles to list object using linq

I have the following array of coordinates:
double[] points = { 1, 2, 3, 4, 5, 6 };
Then I have the following class:
public class clsPoint
{
public double X { get; set; }
public double Y { get; set; }
}
I need to copy the points into List objects. Where the first point in the array is the X and the second point in the array is the Y. Here is what I have so far but it is not correct:
List<clsPoint> lstPoints = points
.Select(coord => new clsPoint
{
X = coord[0],
Y = coord[1]
}).ToList();
Expected Results
clsPoint Objects List (lstPoints)
X = 1 , Y = 2
X = 3 , Y = 4
X = 5 , Y = 6
Any help would be appreciated. Thanks.
You can generate a sequence of consecutive values until the half your array, then you can project using those values as index to get the pairs.
var result=Enumerable.Range(0, points.Length / 2).Select(i=>new clsPoint{X=points[2*i],Y=points[2*i+1]});
Update
This is another solution using Zip extension method and one overload of Where extension method to get the index:
var r2 = points.Where((e, i) => i % 2 == 0)
.Zip(points.Where((e, i) => i % 2 != 0), (a, b) => new clsPoint{X= a, Y= b });
I think there is probably a better way for you to compose your points prior to feeding them into your class. A simple for loop may suffice better in this situation as well.
However, in LINQ, you would first use a projection to gather the index so that you could group based on pairs and then use a second projection from the grouping to populate the class.
It looks like this
points.Select((v,i) => new {
val = v,
i = i
}).GroupBy(o => o.i%2 != 0 ? o.i-1 : o.i).Select(g => new clsPoint() {
X = g.First().val,
Y = g.Last().val
});
Using the overload of Select that receives the current index you can set a grouping rule (in this case a different id for each 2 numbers), then group by it and eventually create your new clsPoint:
double[] points = { 1, 2, 3, 4, 5, 6 };
var result = points.Select((item, index) => new { item, index = index / 2 })
.GroupBy(item => item.index, item => item.item)
.Select(group => new clsPoint { X = group.First(), Y = group.Last() })
.ToList();
Doing it with a simple for loop would look like:
List<clsPoint> result = new List<clsPoint>();
for (int i = 0; i < points.Length; i += 2)
{
result.Add(new clsPoint { X = points[i], Y = points.ElementAtOrDefault(i+1) });
}

LINQ select multiple columns as separate array

I have table 4 columns.
JobId StateId Salary Expense
1 1 35,000 31,000
1 1 33,000 25,000
1 2 28,000 26,000
2 2 7,000 16,000
2 2 6,000 20,000
2 1 9,000 22,000
2 1 15,000 29,000
By using LINQ in C#, i want to group by JobId and StateId combination.For each combination i want an array of Salary and array of Expense.
I can get one column as a array by for each combination, by using this
(from r in myTable.AsEnumerable()
group r by new {
jobId = r.Field<int>("JobId"),
stateId = r.Field<int>("StateId")
}).ToDictionary(
l => Tuple.Create(l.Key.jobId, l.Key.stateId),
l=> (from i in l select i.Field<double>("Salary")).AsEnumerable()
);
How can i have Salary and Expense in two array for each group??
My goal is to find average Salary and average Expense for each combination and do some other operation. Or at least tell me how can select multiple columns as separate array.
Note: I don't want collection of anonymous objects for each combination.
To select two different columns as collections in your query you can do this:
var result =
(from r in myTable.AsEnumerable()
group r by new
{
jobId = r.Field<int>("JobId"),
stateId = r.Field<int>("StateId")
} into g
select new
{
g.Key,
Salaries = g.Select(x => x.Field<double>("Salary")),
Expenses = g.Select(x => x.Field<double>("Expense"))
})
.ToDictionary(
l => Tuple.Create(l.Key.jobId, l.Key.stateId),
l => new { l.Salaries, l.Expenses }
);
Then you can compute the averages fairly easily:
var averageSalary = result[...].Salaries.Average();
var averageExpense = result[...].Expenses.Average();
But if all you really need is the averages, this will work:
var result =
(from r in myTable.AsEnumerable()
group r by new
{
jobId = r.Field<int>("JobId"),
stateId = r.Field<int>("StateId")
} into g
select new
{
g.Key,
AverageSalary = g.Average(x => x.Field<double>("Salary")),
AverageExpense = g.Average(x => x.Field<double>("Expense"))
})
.ToDictionary(
l => Tuple.Create(l.Key.jobId, l.Key.stateId),
l => new { l.AverageSalary, l.AverageExpense }
);
Do not use LINQ for this kind of statement. If you need to count the groupings and the compute some sort of average salary/expense you could try a list:
List<myType> myList = new List<myType>();
//add stuff to myList
List<myType> JobID1 = new List<myType();
List<myType> JobID2 = new List<myType();
foreach(var item in myList)
{
if(item.JobID == 1)
JobID1.add(item);
if(item.JobID == 2)
JobID2.add(item);
}
int avgSalOne;
foreach(var item in JobID1)
{
avgSalOne += item.Salary;
}
avgSalOne = avgSaleOne / JobID2.Count;
//Note that you get Job Id 2 average salary the same way, and also the Expense by changing item. Salary to item.Expense

C# Linq Average

I have a table with data similar to below:
Group TimePoint Value
1 0 1
1 0 2
1 0 3
1 1 3
1 1 5
I want to project a table as such:
Group TimePoint AverageValue
1 0 2
1 1 4
EDIT: The data is in a datatable.
Anybody any ideas how this can be done with LINQ or otherwise?
Thanks.
You need to perform Group By
The linq you need is something like:
var query = from item in inputTable
group item by new { Group = item.Group, TimePoint = item.TimePoint } into grouped
select new
{
Group = grouped.Key.Group,
TimePoint = grouped.Key.TimePoint,
AverageValue = grouped.Average(x => x.Value)
} ;
For more Linq samples, I highly recommend the 101 Linq samples page - http://msdn.microsoft.com/en-us/vcsharp/aa336747#avgGrouped
Here's a more function-oriented approach (the way I prefer it). The first line won't compile, so fill it in with your data instead.
var items = new[] { new { Group = 1, TimePoint = 0, Value = 1} ... };
var answer = items.GroupBy(x => new { TimePoint = x.TimePoint, Group = x.Group })
.Select(x => new {
Group = x.Key.Group,
TimePoint = x.Key.TimePoint,
AverageValue = x.Average(y => y.Value),
}
);
You can do:
IEnumerable<MyClass> table = ...
var query = from item in table
group item by new { item.Group, item.TimePoint } into g
select new
{
g.Key.Group,
g.Key.TimePoint,
AverageValue = g.Average(i => i.Value)
};
Assuming a class like this:
public class Record
{
public int Group {get;set;}
public int TimePoint {get;set;}
public int Value {get;set;}
}
var groupAverage = from r in records
group r by new { r.Group, r.TimePoint } into groups
select new
{
Group = groups.Key.Group,
TimePoint = groups.Key.TimePoint,
AverageValue = groups.Average(rec => rec.Value)
};

Categories