Grouping data between ranges using LINQ in C# - 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();
}

Related

Return a list of string using LINQ through a loop of conditionals

I have a class named Skill and I received a list of it through a parameter and I need to create a list of strings by LINQ that has some rules.
My Class
public class Skill {
public int id {get;set;}
public int year {get;set;}
public int xp {get;set;}
}
Dummy data:
var skills = new List<Skill>(){
new Skill() { id=1, year = 9, xp = 95 } ,
new Skill() { id=2, year = 5 } ,
};
Rules:
// year goes at max 10
// xp goes at max 100
The list of strings I must create is like this:
for each year until 10 plus xp until 100 (if has)
// '1-9-95'
// '1-9-96'
// '1-9-97'
// '1-9-98'
// '1-9-99'
// '1-9-99'
// '1-9-100'
// '1-10-95'
// '1-10-96'
// '1-10-97'
// '1-10-98'
// '1-10-99'
// '1-10-99'
// '1-10-100'
// '2-5'
// '2-6'
// '2-7'
// '2-8'
// '2-9'
// '2-10'
I got it using for statement, but I was wondering about using LINQ.
You need SelectMany and Enumerable.Range:
int maxYear = 10, maxXp = 100;
List<string> resultList = skills
.Where(skill => skill.year <= maxYear && skill.xp <= maxXp) // skip invalid
.SelectMany(skill => Enumerable.Range(skill.year, maxYear - skill.year + 1)
.SelectMany(y => Enumerable.Range(skill.xp, maxXp - skill.xp + 1)
.Select(xp => $"{skill.id}-{y}-{xp}")))
.ToList();
.NET Fiddle: https://dotnetfiddle.net/c80wJs
I think i have overlooked that "(if has)", so you want to list xp only if available:
int maxYear = 10, maxXp = 100;
List<string> resultList = skills
.Where(skill => skill.year <= maxYear && skill.xp <= maxXp) // skip invalid
.SelectMany(skill => Enumerable.Range(skill.year, maxYear - skill.year + 1)
.SelectMany(y => Enumerable.Range(skill.xp, skill.xp == 0 ? 1 : maxXp - skill.xp + 1)
.Select(xp => skill.xp > 0 ? $"{skill.id}-{y}-{xp}" : $"{skill.id}-{y}")))
.ToList();
.NET-fiddle for this (thanks to Rand Random): https://dotnetfiddle.net/06BIqg

Optimize result list using LINQ

I'm getting following data from my Stored procedure
Brand Start End
----------------------------------------
Nike 0 4
Adidas 0 5
Nike 4 10
Levis 0 3
Adidas 5 8
I want to check if there is any range data for given start and end numbers and if there is any data with given range i want to get maximum "End" number of each brand
ex: assume that i want to check whether there is any data for gap 2 to 6
in this case;
for NIKE:
NIKE has ranges 0-4 and 4-10. So its within my 2-6 range (2 is in between 0-4 and 6 is in between 4-10) So i want my result as "NIKE 10"
for ADIDAS: ADIDAS has ranges 0-5 and 5-8. So it also within 2-6 range (2 is in between 0-5 and 6 is in between 5-8) I want it as "ADIDAS 8"
for LEVIS: LEVIS has one range 0-3 and 2 is in between that range. So i want it as "LEVIS 3"
i wrote a Linq query for that and i want to make sure that it's working fine.
var result = (from items in responce.List
where items.Start>= 2 && items.End <= 6
group items by items.Brand into g
select new
{
Max = g.Max(x=> x.End)
});
the result should contain;
NIKE 10
ADIDAS 8
LEVIS 3
Thanks in advance
You are almost there, took me a minute to understand what you were after, but all you need to do is treat each number in your range as an independent value.
That means the database range can have either the 2, the 6 or both between its values.
all you need to do is rewrite the linq to something like this:
var result = from item in list
where (item.Start <= 2 && item.End >= 2) || (item.Start <= 6 && item.End >= 6)
group item by item.Brand into g
select new
{
Brand = g.Key,
Max = g.Max(x => x.End)
};
The only change being
where (item.Start <= 2 && item.End >= 2) || (item.Start <= 6 && item.End >= 6)
All these does is check if any range has 2 or if any range has 6 in it.
For a complete example see this gist.
EDIT:
Try this one, it should always show the max range for the brand, if the values specified exists inside that brand:
var result = (
from item in list
group item by item.Brand into g
from subItem in g
where (subItem.Start <= 2 && subItem.End >= 2) || (subItem.Start <= 6 && subItem.End >= 6)
select new
{
Brand = g.Key,
Max = g.Max(x => x.End)
}
).Distinct();
Here's another gist with the updated linq.
Try this :
static void Main(string[] args)
{
List<Sneaker> sneakers = new List<Sneaker>() {
new Sneaker() { brand = "Nike", start = 0, end = 4},
new Sneaker() { brand = "Adidas", start = 0, end = 5},
new Sneaker() { brand = "Nike", start = 4, end = 10},
new Sneaker() { brand = "Levis", start = 0, end = 3},
new Sneaker() { brand = "Adidas", start = 5, end = 8}
};
int start = 2;
int end = 6;
var groups = sneakers.GroupBy(x => x.brand).Select(x => x.OrderBy(y => y.end)).Select(x => x.Any(y => y.end > end) ? x.FirstOrDefault(): x.LastOrDefault())
.Where(x => x != null).Select(x => new {brand = x.brand, end = x.end}).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) });
}

How to group in Linq based on previous Value

I want to group a pointcloud based on 2 conditions
simple on Y so I wrote pointcloudH.GroupBy(KVP => KVP.Value.Y) where KVP is an KeyValuePair<string,System.Drawing.Point>
and now I want to group it also by X if X == (previousX + 1)
as far as I know I should us ThenBy() but what do I have to write between the brackets?
and here an example for a better illustration what I want to achieve
Sample pointcloud
(x|y) (1|1),(2|1),(4|1),(1|2),(2|3),(3|3),(4|3),(5|8),(9|10)
after step 1. it looks like this
group1 (1|1),(2|1),(4|1)
group2 (1|2)
group3 (2|3),(3|3),(4|3)
group4 (5|8)
group5 (9|10)
after step 2. it should look like this
group1 (1|1),(2|1)
group2 (4|1)
group3 (1|2)
group4 (2|3),(3|3),(4|3)
group5 (5|8)
group6 (9|10)
current code
var Hgroup = pointcloudH.OrderBy(KVP => KVP.Value.Y) // order by Y
.GroupBy(KVP => KVP.Value.Y) // groub by Y
.ThenBy(KVP => KVP.Value.X); // group by X ???
I don't think LINQ is the best tool for this kind of job, but it can be achieved. The important part is to think of the relation between your Point.X and the index of the relative Point in the Point.Y group. Once you realize you want to group them by Point.X - Index, you can do:
var Hgroup = pointcloudH.OrderBy(p => p.Y)
.GroupBy(p => p.Y)
.SelectMany(yGrp =>
yGrp.Select((p, i) => new {RelativeIndex = p.X - i, Point = p})
.GroupBy(ip => ip.RelativeIndex, ip => ip.Point)
.Select(ipGrp => ipGrp.ToList()))
.ToList();
Note that this will probably perform worst than a regular iterative algorithm. My pointcloudH is an array, but you can just change the lambda to reflect your own list. Also, remove the ToList() if you want to defer execution. This was to ease the result inspection in the debugger.
If you want to group all points in a Point.Y group regardless of their index (ie order by Point.X as well. Add ThenBy(p => p.X) after the first OrderBy clause.
Your problem cannot be solved by doing 2 separate group by clauses. I have created a little sample which should work for your problem. These are the key things that are happening in the code:
Construct 'mirror' array and insert a copy of the first item at index 0, this is used to keep track of the previous point
Create a variable that is incremented whenever a 'chain' is broken. This is whenever the next value is not equal to the previous + 1. This way we can group by an unique key per 'chain'.
class Program
{
public struct Point
{
public static Point Create(int x, int y)
{
return new Point() { X = x, Y = y };
}
public int X { get; set; }
public int Y { get; set; }
public override string ToString()
{
return string.Format("({0}|{1})", X, Y);
}
}
static void Main(string[] args)
{
//helper to avoid to much keystrokes :)
var f = new Func<int, int, Point>(Point.Create);
//compose the point array
//(1|1),(2|1),(4|1),(1|2),(2|3),(3|3),(4|3),(5|8),(9|10)
var points = new[] { f(1, 1), f(2, 1), f(4, 1), f(1, 2), f(2, 3), f(3, 3), f(4, 3), f(5, 8), f(9, 10) }.OrderBy(p => p.Y).ThenBy(p => p.X);;
//create a 'previous point' array which is a copy of the source array with a item inserted at index 0
var firstPoint = points.FirstOrDefault();
var prevPoints = new[] { f(firstPoint.X - 1, firstPoint.Y) }.Union(points);
//keep track of a counter which will be the second group by key. The counter is raised whenever the previous X was not equal
//to the current - 1
int counter = 0;
//the actual group by query
var query = from point in points.Select((x, ix) => new { current = x, prev = prevPoints.ElementAt(ix) })
group point by new { point.current.Y, prev = (point.prev.X == point.current.X - 1 ? counter : ++counter) };
//method chaining equivalent
query = points.Select((x, ix) => new { current = x, prev = prevPoints.ElementAt(ix) })
.GroupBy(point => new { point.current.Y, prev = (point.prev.X == point.current.X - 1 ? counter : ++counter) });
//print results
foreach (var item in query)
Console.WriteLine(string.Join(", ", item.Select(x=> x.current)));
Console.Read();
}
}

Efficient way to merge table like datastructure using linq

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

Categories