Optimize result list using LINQ - c#

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

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

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

How to append more rows to a Linq list after the condition is met

I have a list which contains items (let assume 1000 items). I want to select data from that list where TimeInSecond matches the criteria.
newList = oldList.Where(x => x.TimeInSecond >= 30 && x.TimeInSecond <= 90).ToList();
// the above query returns 20 items (from 10 to 20)
However, I need to append next N number of rows from the oldList
// This is just an example of what I need, for example 10 next more items
newList = oldList.Where(x => x.TimeInSecond >= 30 && x.TimeInSecond <= 90).GetNextMoreItems(10).ToList() ;
// the above query returns (20 + 10) 30 items (from 1 to 30)
Since you mentioned in the comments, you want 10 additional elements from the last element where condition is true (satisfied), you could do something like this.
// Get the elements where condition is satisfied.
newList = oldList
.Where(x => x.TimeInSecond >= 30 && x.TimeInSecond <= 90)
.ToList() ;
// Find the index of last element and then take 10 elements from then on.
newList = newList.Concat(oldList
.Skip(oldList.ToList().IndexOf(newList.Last() + 1))
.Take(10))
.ToList();
Check sample Demo to see how it works.
You can use Union or AddRange to append your new 10 items to your existing list. Of course, the data-types has to be the same. Here's an example with int type:
List<int> l1 = new List<int>() {1,2,3 };
List<int> l2 = new List<int>() { 1,4,5,6};
l1.AddRange(l2); // l1 now contains {1,2,3,1,4,5,6}
// will remove duplicates using default Equality Comparer
var l3 = l1.Union(l2).ToList(); // contains {1,2,3,4,5,6}
With your example, it may look like this:
var list1 = oldList.Where(x => x.TimeInSecond >= 30 && x.TimeInSecond <= 90);
var list2 = GetNextMoreItems(10).ToList();
var finalList = list1.AddRange(list2);
Edit based on comment
To exclude items that you've already selected, you can use Except, which is opposite of Intersect . So, for the int example above:
var l4 = l3.Except(l1); // contains {4,5,6}
And then, to pick certain number of elements, append .Take(count).
If you want those that match and N of which other items in the collection you can simply do:
Option 1: where condition or counter
int n = 10;
int counter = 0;
var values = oldList.Where(x => (x.TimeInSecond >= 30 && x.TimeInSecond <= 90) ||
counter++ < n).ToList();
Option 2: use a helping method to avoid repeating condition
Func<dynamic, bool> condition = (x) => x.TimeInSecond >= 30 && x.TimeInSecond <= 90;
var result = oldList.Where(x => condition(x))
.Concat(oldList.Where(x => !condition(x)).Take(n));
If order matters and you want to take N items that come only after all those that match:
Option 1:
int n = 10;
var values = oldList.Select((item, index) => new { item, index })
.Where(x => x.item.TimeInSecond >= 30 && x.item.TimeInSecond <= 90);
var lastItem = values.Max(item => item.index);
var result = values.Select(item => item.item)
.Concat(oldList.Skip(lastItem + 1).ToList().Take(n));
Option 2: Maybe looks a bit cleaner.. not sure (and has to have the ToList() just before the concat:
int n = 1;
int lastItem = 0;
var result = oldList.Where((x, i) =>
{
var outcome = x.TimeInSecond >= 30 && x.TimeInSecond <= 90;
if (outcome)
lastItem = i;
return outcome;
}).ToList()
.Concat(oldList.Skip(lastItem + 1).ToList().Take(n)); //Result: 40, 50, 100 (from data below)
Tested with:
List<dynamic> oldList = new List<dynamic>()
{
new {TimeInSecond = 10},
new {TimeInSecond = 40},
new {TimeInSecond = 50},
new {TimeInSecond = 100},
new {TimeInSecond = 120},
};

Split large integer array into groups of multiples using one performant LINQ query

I want to sort a large integer array into 2 groups, i.e. 1 group the multiples of 4 and the other group the multiples of 5. How can I do this using just one query? Keep an eye on the performance which is really important in my case.
To further explain what I need, suppose my list of numbers is { 2, 7, 8, 10, 12, 14, 19,20, 25} then I would expect my output to be this:
new[]
{
new
{
Remainder = 4,
Numbers = new List<int>(){ 8, 12, 20}
},
new
{
Remainder = 5,
Numbers = new List<int>(){10, 20, 25}
}
}
Here's what I have gotten so far:
var numberGroupsTimes5 =
from n in numbers
group n by n % 5 into g
where g.Key == 0
select new { Remainder = g.Key, Numbers = g };
var numberGroupsTimes4 =
from n in numbers
group n by n % 4 into g
where g.Key == 0
select new { Remainder = g.Key, Numbers = g };
As you can see it gets me close with 2 queries but as I said I would like a single query.
You could use Concat:
var something = numberGroupsTimes5.Concat(numberGroupsTimes4);
to simply concatenate two sequences.
It's not entire clear why you use a GroupBy, then filter for Key == 0. Remainder will always be 0.
Maybe a simple Where is enough?
You can simply "combine" your queries by using a logical OR (||):
var something = numbers.Where(x => x%4 == 0 || x%5 == 0);
In response to your comment: Are you looking for something like this?
var result = new[] {4, 5}
.Select(d => new
{
Divider = d,
Values = numbers.Where(n => n % d == 0).ToList()
});
Do you mean?
var numberGroupsTimes4or5 = from n in numbers
group n by n into g
where g.Key % 4 == 0 || g.Key % 5 == 0
select new { Remainder = g.Key, Numbers = g };
Maybe this?
var result = new[] { 4, 5 }
.SelectMany(x => numbers.Select(n => (n, x)))
.Where(g => g.n % g.x == 0)
.GroupBy(g => g.x, (Key, g) =>
new { Remainder = Key, Numbers = g.Select(z => z.n) });
which gives this result
Here is a similar approach but this time using a query syntax like in your question.
var numbersAndRemainders = new[] { 4, 5 }
.SelectMany(rem => numbers.Select(n => (n, rem)));
var numberGroups =
from n in numbersAndRemainders
group n by new { remainder = n.n % n.rem, n.rem } into g
where g.Key.remainder == 0
select new { Remainder = g.Key.rem, Numbers = g.Select(z => z.n) };
There are two LINQ methods you could use for this:
//This will join the lists, excluding values that already appear once
var result = numberGroupsTimes5.Union(numberGroupsTimes4)
//This will simply append one list the the other
var result = numberGroupsTimes5.Concat(numberGroupsTimes4)

Linq group by month/year with empty months for past 12 months

How would I get this query to get the monthly count data for the past 12 months? I don't want to hard code the range, I want to use the current date DateTime.Now and get all the data for the past 12 months from that. I am trying to avoid adding a calendar table to the database and do this just using LINQ.
Some months might not have any data but I still need a count of 0 for those.
For example. If my data contains
Date Count
12/2/2013, 4
10/1/2014, 1
11/5/2014, 6
The results should be, using the current date of 11/9/2014
11/2013, 0
12/1013, 4
1/2014, 0
2/2014, 0
3/2014, 0
4/2014, 0
5/2014, 0
6/2014, 0
7/2014, 0
8/2014, 0
9/2014, 0
10/2014, 1
11/2014, 6
I can't get it to work. I think it's how I'm using Range but I'm not sure.
TimeSpan ts = new TimeSpan(365, 0, 0, 0);
DateTime yearAgo = DateTime.Now.Subtract(ts);
var changesPerYearAndMonth =
from year in Enumerable.Range(yearAgo.Year, 1)
from month in Enumerable.Range(1, 12)
let key = new { Year = year, Month = month }
join revision in list on key
equals new { revision.LocalTimeStamp.Year,
revision.LocalTimeStamp.Month } into g
select new { GroupCriteria = key, Count = g.Count() };
I have modified the answer from this this link as a starting point.
Linq: group by year and month, and manage empty months
I just found this article that is the same question but unanswered
Linq - group by datetime for previous 12 months - include empty months
To get the past twelve months, use
var now = DateTime.Now;
var months = Enumerable.Range(-12, 12)
.Select(x => new {
year = now.AddMonths(x).Year,
month = now.AddMonths(x).Month });
To be safe you should first move 'now' to the start of the month to avoid any end-of-month effects with AddMonth.
var now = DateTime.Now;
now = now.Date.AddDays(1-now.Day);
Complete example:-
var list = new [] {
new { LocalTimeStamp = DateTime.Parse("12/2/2013"), count = 4},
new { LocalTimeStamp = DateTime.Parse("10/1/2014"), count = 1 },
new { LocalTimeStamp = DateTime.Parse("11/5/2014"), count = 6}
};
var now = DateTime.Now;
now = now.Date.AddDays(1-now.Day);
var months = Enumerable.Range(-12, 13)
.Select(x => new {
year = now.AddMonths(x).Year,
month = now.AddMonths(x).Month });
var changesPerYearAndMonth =
months.GroupJoin(list,
m => new {month = m.month, year = m.year},
revision => new { month = revision.LocalTimeStamp.Month,
year = revision.LocalTimeStamp.Year},
(p, g) => new {month = p.month, year = p.year,
count = g.Sum(a => a.count)});
foreach (var change in changesPerYearAndMonth)
{
Console.WriteLine(change.month + " " + change.year +" " + change.count);
}
You don't need a 3-way join, you just need to filter your data before grouping.
1) Query expression syntax
// since your list item type was not posted, anyway same access as your LocalTimeStamp property
list = new List<DateTime>();
DateTime aYearAgo = DateTime.Now.AddYears(-1);
var dateslastYear = from date in list
where date > aYearAgo
group date by new { date.Year, date.Month } into g
select new { GroupCriteria = g.Key, Count = g.Count() };
2) Chained
dateslastYear = list.Where (d=>d>aYearAgo)
.GroupBy (date=>new{date.Year, date.Month });
3) If you want grouping by year/month pairs, including records of not existent entries, and also omitting those pairs that are older than a year occurring with the joined Enumerable.Range call:
var thisYearPairs = from m in Enumerable.Range(1, DateTime.Now.Month)
select new { Year = DateTime.Now.Year, Month = m };
var lastYearPairs = from m in Enumerable.Range(DateTime.Now.Month, 12 - DateTime.Now.Month + 1)
select new { Year = DateTime.Now.Year - 1, Month = m };
var ymOuter = from ym in thisYearPairs.Union(lastYearPairs)
join l in list on new { ym.Year, ym.Month } equals new { l.Year, l.Month } into oj
from p in oj.DefaultIfEmpty()
select new { a = ym, b = p == null ? DateTime.MinValue : p };
var ymGroup = from ym in ymOuter
group ym by ym into g
select new { GroupCriteria = g.Key.a, Count = g.Key.b == DateTime.MinValue ? 0 : g.Count() };
You are taking the range for the 12 months of last year only but you actually want the last twelve months.
You can do this using a Enumerable.Range and the AddMonths method:
var changesPerYearAndMonth =
from month in Enumerable.Range(0, 12)
let key = new { Year = DateTime.Now.AddMonths(-month).Year, Month = DateTime.Now.AddMonths(-month).Month }
join revision in list on key
equals new
{
revision.LocalTimeStamp.Year,
revision.LocalTimeStamp.Month
} into g
select new { GroupCriteria = key, Count = g.Count() };
public int YearDiff(DateTime a, DateTime b)
{
return (int) Math.Floor((a.Year + a.Month / 100.0 + a.Day / 10000.0) - (b.Year + b.Month / 100.0 + b.Day / 10000.0));
}

Categories