I'm trying to group my list using linq by an interval of 30 minutes.
Let’s say we have this list:
X called at 10:00 AM
Y called at 10:10 AM
Y called at 10:20 AM
Y called at 10:35 AM
X called at 10:40 AM
Y called at 10:45 AM
What i need is to group these items in a 30 minutes frame and by user, like so:
X called at 10:00 AM
Y called 3 times between 10:10 AM and 10:35 AM
X called at 10:40 AM
Y called at 10:45 AM
Here's what i'm using with Linq:
myList
.GroupBy(i => i.caller, (k, g) => g
.GroupBy(i => (long)new TimeSpan(Convert.ToDateTime(i.date).Ticks - g.Min(e => Convert.ToDateTime(e.date)).Ticks).TotalMinutes / 30)
.Select(g => new
{
count = g.Count(),
obj = g
}));
I need the result in one list, but instead im getting the result in nested lists, which needs multiple foreach to extract.
Any help is much appreciated!
I think you are looking for SelectMany which will unwind one level of grouping:
var ans = myList
.GroupBy(c => c.caller, (caller, cg) => new { Key = caller, MinDateTime = cg.Min(c => c.date), Calls = cg })
.SelectMany(cg => cg.Calls.GroupBy(c => (int)(c.date - cg.MinDateTime).TotalMinutes / 30))
.OrderBy(cg => cg.Min(c => c.date))
.ToList();
Note: The GroupBy return selects the Min as a minor efficiency improvement so you don't constantly re-find the minimum DateTime for each group per call.
Note 2: The (int) conversion creates the buckets - otherwise, .TotalMinutes returns a double and the division by 30 just gives you a (unique) fractional answer and you get no grouping into buckets.
By modifying the initial code (again for minor efficiency), you can reformat the answer to match your textual result:
var ans = myList
.GroupBy(c => c.caller, (caller, cg) => new { Key = caller, MinDateTime = cg.Min(c => c.date), Calls = cg })
.SelectMany(cg => cg.Calls.GroupBy(c => (int)(c.date - cg.MinDateTime).TotalMinutes / 30), (bucket, cg) => new { FirstCall = cg.MinBy(c => c.date), Calls = cg })
.OrderBy(fcc => fcc.FirstCall.date)
.ToList();
var ans2 = ans.Select(fcc => new { Caller = fcc.FirstCall.caller, FirstCallDateTime = fcc.FirstCall.date, LastCallDateTime = fcc.Calls.Max(c => c.date), Count = fcc.Calls.Count() })
.ToList();
Instead of grouping by a DateTime, try grouping by a key derived from the date.
string GetTimeBucketId(DateTime time) {
return $"${time.Year}-{time.Month}-{time.Day}T{time.Hour}-{time.Minute % 30}";
}
myList
.GroupBy(i => GetTimeBucketId(i.caller.date))
.Select(g => { Count = g.Count(), Key = g.Key });
Related
I have a list where I'm applying the following condition with linQ:
I want to select all items where Name contains a certain string.
var nameFilter = result
.Where(e => e.Name.Contains(requestedValue))
.ToList();
At the end, sometimes it happens that I am having a list with repeated names:
For example:
requestedValue = 'form';
I end up with:
Name Price
transformer 100
transformer 20
formation 340
former 201
I got transformer twice. In that case, I want to only leave transformer with the least price : 20
How could I do this with linQ without looping?
You can take advantage of GroupBy method
var nameFilter = result.Where(e => e.Name.Contains(requestedValue))
.GroupBy(k=>k.Name, g=>g.Price, (k,g)=>new Model {Name = k, Price = g.Min()})
.ToList();
where new Model should be changed to your class name.
If you have more properties to return probably it will be more convenient to do
var nameFilter = result.Where(e => e.Name.Contains(requestedValue))
.GroupBy(k => k.Name, g => g, (k, g) =>
{
var minPrice = g.Min(x => x.Price);
return g.First(x => x.Price == minPrice);
}).ToList();
Finding minPrice and finding the item with minPrice can be done is a single for loop or, for example, by using following discussion here
I have a table called 'Samples' with the following columns: Id, Type, Quantity, Unit, SampleTime.
I have the following statement that calculates the daily weight average:
weightchart.data = db.Samples
.Where(n => n.Type == "weight")
.GroupBy(n => DbFunctions.TruncateTime(n.SampleTime))
.Select(item => new ChartData()
{
timestamp = item.Key,
average = item.Average(a => a.Quantity),
max = item.Max(a => a.Quantity),
min = item.Min(a => a.Quantity),
count = item.Count()
}).OrderBy(n => n.timestamp).ToList();
charts.Add(weightchart);
Now I would like to get the hourly average. I can't figure out how to do this. I'm new to c#, linq and entities.
Update: I've updated my statement to this:
weightchart.data = db.Samples.Where(n => n.Type == "weight").GroupBy(n => new { Date = n.SampleTime.Date, Hour = n.SampleTime.Hour }).Select(item => new ChartData()
{
timestamp = new DateTime(item.Key.Date.Year, item.Key.Date.Month, item.Key.Date.Day, item.Key.Date.Hour, 0, 0),
average = item.Average(a => a.Quantity),
max = item.Max(a => a.Quantity),
min = item.Min(a => a.Quantity),
count = item.Count()
}).OrderBy(n => n.timestamp).ToList();
charts.Add(weightchart);
However I get this exception:
An exception of type 'System.NotSupportedException' occurred in EntityFramework.SqlServer.dll but was not handled in user code
Additional information: The specified type member 'Date' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
see update below
Assuming the type of SampleTime is a System.DateTime, you could change the GroupBy to .GroupBy(n => n.SampleTime.Hour), and that would let you compute the average per hour. Be aware that DateTime.Hour returns a hours from 0 to 23.
Try something like the following. I've slightly simplified, but if you can verify that it works, you can reshape it to the full thing you are trying. Also, it's hard for me to test it, because I'd have construct sample data first). An explanation for what is going on (a LINQ to SQL limitation) is here and a helpful hint on how to more easily get around the limitation via AsEnumerable() is here.
var dataAsList = (from n in db.Samples.Where(n => n.Type == "weight").AsEnumerable()
group n by new { Date = n.SampleTime.Date, Hour = n.SampleTime.Hour } into g
select new
{
dategrouping = g.Date,
hourgrouping = g.Hour,
average = g.Average(a => a.Quantity),
max = g.Max(a => a.Quantity),
min = g.Min(a => a.Quantity),
count = g.Count()
}).ToList();
My data structure:
BrowserName(Name) Count(Y)
MSIE9 7
MSIE10 8
Chrome 10
Safari 11
-- and so on------
What I'm trying to do is get the top 10 and then get the sum of rest and call it 'others'.
I'm trying to get the others as below but geting error..
Data.OrderBy(o => o.count).Skip(10)
.Select(r => new downModel { modelname = "Others", count = r.Sum(w => w.count) }).ToList();
The error is at 'r.Sum(w => w.count)' and it says
downModel does not contain a definition of Sum
The downModel just has string 'modelname' and int 'count'.
Any help is sincerely appreciated.
Thanks
It should be possible to get the whole result - the top ten and the accumulated "others" - in a single database query like so:
var downModelList = context.Data
.OrderByDescending(d => d.Count)
.Take(10)
.Select(d => new
{
Name = d.Name,
Count = d.Count
})
.Concat(context.Data
.OrderByDescending(d => d.Count)
.Skip(10)
.Select(d => new
{
Name = "Others",
Count = d.Count
}))
.GroupBy(x => x.Name)
.Select(g => new downModel
{
modelName = g.Key,
count = g.Sum(x => x.Count)
})
.ToList();
If you want to create just one model, then get the sum first and create your object:
var count = Data.OrderBy(o => o.count).Skip(10).Sum(x => x.count);
var model = new downModel { modelname = "Others", count = count };
Btw, OrderBy performs a sort in ascending order. If you want to get (or Skip) top results you need to use OrderByDescending.
I have a list of person and I want count of person register every hour. I have used below GroupBy clause and I got the correct result.
var persons = lstPerson.GroupBy(x =>(x.CreatedOn.Hour))
.Select(grp => new { total = grp.Count(), key = grp.Key })
.OrderBy(x => x.key)
.ToList();
But I want for every hour. It only shows the value in which count is there. If for 1st hour no person is registered then it doesn't show 0 count in list.
So for example there are person registered only for 13, 14,15 hours(means at 13:00,14:00 and 15:00 hours) then it shows the count for it but not for other hours.
A couple of options:
Firstly, and with least change to your code, you can just "add one" to each group and then "subtract one" from each count:
var persons = lstPerson
.Select(x => (x.CreatedOn.Hour)) // Get the hours from the people
.Concat(Enumerable.Range(0, 24)) // Add an extra copy of each hour
.GroupBy(h => h) // ↓ subtract the extra hours
.Select(grp => new { total = grp.Count() - 1, key = grp.Key })
.OrderBy(x => x.key)
.ToList();
Secondly, more tidy but involves replacing all of your code, you can join the list of people onto the list of hours:
var persons = Enumerable.Range(0, 24)
.GroupJoin(
lstPerson,
h => h, // Correlate the hours in the range
p => p.CreatedOn.Hour, // with the hours from each person
(h, ps) => new { total = ps.Count(), key = h))
.ToList(); // ↑ This selects one element for each hour in the range
I have a linq statement that averages the rows in a DataTable and displays them on a chart, grouped by date and time of day.
There are 1 big problem: there are many 0 values that are returned, due to particular times of day simply not having anything going on. These are skewing my averages something awful
Different times of day may have 0s in different columns, so I can't just delete each row with a 0 in the columns (cleanly), as I would end up with no rows left in the datatable, or at least I can't think of a clean way to do it in any case.
This is what I have:
var results = from row2 in fiveDayDataTable.AsEnumerable()
group row2 by ((DateTime)row2["TheDate"]).TimeOfDay
into g
select new
{
Time = g.Key,
AvgItem1 = g.Average(x => (int)x["Item1"]),
AvgItem2 = g.Average(x => (int)x["Item2"]),
AvgItem3 = g.Average(x => (int)x["Item3"]),
AvgItem4 = g.Average(x => (int)x["Item4"]),
AvgItem5 = g.Average(x => (int)x["Item5"]),
};
I don't know if this is possible, so I figured I would ask- is there a way to do the average without the 0s?
Thank you!
Sure you can filter out the zeros:
AvgItem1 = g.Select(x => (int)x["Item1"]).Where(x => x != 0).Average(),
AvgItem2 = g.Select(x => (int)x["Item2"]).Where(x => x != 0).Average(),
AvgItem3 = g.Select(x => (int)x["Item3"]).Where(x => x != 0).Average(),
AvgItem4 = g.Select(x => (int)x["Item4"]).Where(x => x != 0).Average(),
AvgItem5 = g.Select(x => (int)x["Item5"]).Where(x => x != 0).Average(),
If your result set (after the Where) might be empty, you might need to call DefaultIfEmpty.
AvgItem1 = g.Select(x => (int)x["Item1"]).Where(x => x != 0).DefaultIfEmpty(0).Average(),
This will return a non-empty result set so your Average will be able to work with it.
Since you have a lot of repetition, you could consider refactoring your average logic into a separate method or anonymous function:
Func<IEnumerable<YourClass>, string, double> avg =
(g, name) => g.Select(x => (int)x[name]).Where(x => x != 0).Average();
var results = from row2 in fiveDayDataTable.AsEnumerable()
group row2 by ((DateTime)row2["TheDate"]).TimeOfDay
into g
select new
{
Time = g.Key,
AvgItem1 = avg(g, "Item1"),
AvgItem2 = avg(g, "Item2"),
AvgItem3 = avg(g, "Item3"),
AvgItem4 = avg(g, "Item4"),
AvgItem5 = avg(g, "Item5"),
};
Add a Where to each query just before the Average in which you ensure that the item is not equal to zero.