SelectMany c# linq - c#

Sorry to open another post.
.SelectMany with C#
I asked in the previous post but I can't get the solution for my problem.
`
var departments = stops
.Where(stop => stop.InitDate != null)
.SelectMany(stop => new[] { Month = stop.InitDate.Month, Year = stop.InitDate.Year, Duration = stop.Duration })
.GroupBy(dt => new { dt.Month, dt.Year })
.OrderBy(g => g.Key.Month)
.ThenBy(g => g.Key.Year)
.Select(g => new
{
Key = g.Key.Month,
Año = g.Key.Year,
Duration = g.Sum(v => v.Duration),
Count = g.Count()
});
`
This is de final solution to my problem, but, when I use this in my code, I have some problems.
If I don't declare the variables "Month, Year, Duration", I get an error in:
.SelectMany(stop => new[] { Month = stop.InitDate.Month, Year = stop.InitDate.Year, Duration = stop.Duration })
But I do not know what kind of data they are month and year because if I declare it how integer, I get an error in .GroupBy(dt => new { dt.Month, dt.Year }), because the compiler recognizes dt as integer.
I tried to declare Month and Year as integer and put in the .GroupBy this:
.GroupBy(dt => new { Month, Year }) but it is not correct...
Thank you in advance
Raúl

Apparently you have a sequence named Stops which is a sequence of stop objects. Each stop object may or may have not an InitDate. If it has an InitDate, this InitDate has at least properties Month, Year and Duration, which are all int.
What you want is from your original Stops, only those stop objects that have an InitDate. From every stop object you select, you want to create a new object, with a Key property which contains the Month and the Year and a Duration property which contains the Duration.
You were almost there. Your problem was that you used SelectMany instead of a simple Select.
Normally you use SelectMany if you have a sequence of sequences that you want to concatenate into one sequence. However your Stops does not have a sequence of sequences. Every stop object should produce one "new object with Year, Month and Duration".
Or in simple words: whenever you have a collection of "thingies" and you want to convert every "thingy" into exactly one "other thingy", you should use Select, not SelectMany:
In your case the Select will be:
var stopData = stops
.Where(stop => stop.InitDate != null)
.Select(stop => new
{
GroupKey = new
{
Month = stop.InitDate.Month,
Year = stop.InitDate.Year,
},
Duration = stop.Duration
});
I put the Year and the Month alreay in a property GroupKey, because that makes the grouping easier:
var groupsOfSameMonth = stopData
.GroupBy( item => item.Key, item => item.Duration);
Now every group, contains a key, which is the {Month, Year}. The elements of the group are all Durations with this {month, year}. So now all you have to do is from every group, take all elements from the group and Sum() and Count() them:
var durationsOfSameMonth = groupsOfSameMonth
.Select(group => new
{
Month = group.Key.Month, // I assumed your Key was an error
Año = group.Key.Year,
Duration = group.Sum(),
Count = group.Count()
});
All you have to do is some ordering and you are finished.

Related

Using GroupBy to compute average or count based on the whole data until the corresponding date

I have the AssessmentItems DB object which contains the items about: Which user evaluated (EvaluatorId), which submission (SubmissionId), based on which rubric item (or criteria)(RubricItemId) and when (DateCreated).
I group by this object by RubricItemId and DateCreated to get compute some daily statistics based on each assessment criteria (or rubric item).
For example, I compute the AverageScore, which works fine and returns an output like: RubricItem: 1, Day: 15/01/2019, AverageScore: 3.2.
_context.AssessmentItems
.Include(ai => ai.RubricItem)
.Include(ai => ai.Assessment)
.Where(ai => ai.RubricItem.RubricId == rubricId && ai.Assessment.Submission.ReviewRoundId == reviewRoundId)
.Select(ai => new
{
ai.Id,
DateCreated = ai.DateCreated.ToShortDateString(),//.ToString(#"yyyy-MM-dd"),
ai.CurrentScore,
ai.RubricItemId,
ai.Assessment.SubmissionId,
ai.Assessment.EvaluatorId
})
.GroupBy(ai => new { ai.RubricItemId, ai.DateCreated })
.Select(g => new
{
g.Key.RubricItemId,
g.Key.DateCreated,
AverageScore = g.Average(ai => ai.CurrentScore),
NumberOfStudentsEvaluating = g.Select(ai => ai.EvaluatorId).Distinct().Count(),
}).ToList();
What I want to do is to compute the average until that day. I mean instead of calculating the average for the day, I want to get the average until that day (that is, I want to consider the assessment scores of the preceding days). The same why, when I compute NumberOfStudentsEvaluating, I want to indicate the total number of students participated in the evaluation until that day.
One approach to achieve this could be to iterate through the result object and compute these properties again:
foreach (var i in result)
{
i.AverageScore = result.Where(r => r.DateCreated <= i.DateCreated).Select(r => r.AverageScore).Average(),
}
But, this is quite costly. I wonder if it is possible to tweak the code a bit to achieve this, or should I start from scratch with another approach.
If you split the query into two halves, you can compute the average as you would like (I also computed the NumberOfStudentsEvaluating on the same criteria) but I am not sure if EF/EF Core will be able to translate to SQL:
var base1 = _context.AssessmentItems
.Include(ai => ai.RubricItem)
.Include(ai => ai.Assessment)
.Where(ai => ai.RubricItem.RubricId == rubricId && ai.Assessment.Submission.ReviewRoundId == reviewRoundId)
.Select(ai => new {
ai.Id,
ai.DateCreated,
ai.CurrentScore,
ai.RubricItemId,
ai.Assessment.SubmissionId,
ai.Assessment.EvaluatorId
})
.GroupBy(ai => ai.RubricItemId);
var ans1 = base1
.SelectMany(rig => rig.Select(ai => ai.DateCreated).Distinct().Select(DateCreated => new { RubricItemId = rig.Key, DateCreated, Items = rig.Where(b => b.DateCreated <= DateCreated) }))
.Select(g => new {
g.RubricItemId,
DateCreated = g.DateCreated.ToShortDateString(), //.ToString(#"yyyy-MM-dd"),
AverageScore = g.Items.Average(ai => ai.CurrentScore),
NumberOfStudentsEvaluating = g.Items.Select(ai => ai.EvaluatorId).Distinct().Count(),
}).ToList();

Gettiing last items using linq query

I have a list that get from database using entity framework.
var list = context.Items;
list result is like this.
var list = new List<Item>{
new Item { id=1, operation="write", date="23.03.2018 08:25:45" },
new Item { id=1, operation="read", date="23.03.2018 09:40:15" },
new Item { id=1, operation="read", date="23.03.2018 10:15:17" },
new Item { id=1, operation="read", date="23.03.2018 11:46:39" }
}
I want to minify this list by operation and last date.
var min = new List<Item>{
new Item { id=1, operation="write", date="23.03.2018 08:25:45" },
new Item { id=1, operation="read", date="23.03.2018 11:46:39" }
}
I am getting last 'write' operation and last 'read' item.
I have used the concept of numbering the rows based on the date, and then filtering them. Something like:
var groups = list.GroupBy(x => x.operation)
.SelectMany(g =>
g.OrderByDescending(i => i.date).Select((j, i) => new { j.id, j.operation,j.date, rn = i + 1 })
);
var data = groups.Where(i => i.rn == 1).ToList();
You can achieve it with the help of following code
var result = list.GroupBy(x => x.operation)
.Select(x => x.OrderByDescending(y => Convert.ToDateTime(y.date)).FirstOrDefault())
.ToList();
Seems like you need distinct value form the list and then you need to get first item with latest date
List<Item> list = context.Items
.GroupBy(a => a.operation)
.Select(g => g.OrderByDescending(p => Convert.ToDateTime(p.date)).First())
.ToList();
you need to perform group by on operation once its done you should take first item from each group but to take latest by date you need to perform descending on date which will make latest date record first recoed and than you just need to apply first function on it.
You can try the following. Just group by with operation and select the last date in each group.
var lastItems = list.GroupBy(p => p.operation).Select(grp => new
{
grp.Key,
lastOperation = grp.OrderByDescending(p => Convert.ToDateTime(p.date)).First()
}).Select(p=> p.lastOperation);
However, if your date field contains different time formats it may fail to convert to DateTime.
And you need to make sure to convert date field to DateTime otherwise you may get different ordering by string.
Edit: The following does the same thing with less typing:
var lastItems = list.GroupBy(p => p.operation).Select(grp =>
grp.OrderByDescending(p => Convert.ToDateTime(p.date)).First());
Since Max is (much) faster than sorting, use Max. Unfortunately your date field is not a DateTime, so conversion is needed. I did the conversion first so I wouldn't have to do it twice when filtering. I also didn't assume there would be only one most recent date per operation, if you need only you could add a First. I preserved and returned the original Item at the end.
var min = from item in list
select new { item, date = DateTime.Parse(item.date) } into newitem
group newitem by newitem.item.operation into itemgroup
let maxdate = itemgroup.Max(i => i.date)
from item in itemgroup
where item.date == maxdate
select item.item;
Note: It is possible that .Net Core handles the case of OrderBy.First specially without sorting.

.NET when grouping records by hour impossible to use datetime in the select

I'm trying to group a list of records by hour and store the number of record for each hour. Here is my code :
DateTime firstTimeStamp = myRecords.DataBaseRecords.First().TimeStamp;
Statistics = myRecords.DataBaseRecords
.GroupBy(x => x.TimeStamp.Hour)
.Select(group => new GraphModel() { Date =firstTimeStamp.AddHours(group.Key), Value = group.Count() })
.ToList();
The problem is that when I'm on the select fuction, I cannot acces to the DateTime anymore so the field group.key contains a value between 0 and 24. I just need to group all the records by hour and foreach hour, I need to have the number of records in the Value parameter.
You have to group the data by absolute hours as of the first timestamp, i.e. the differences in hours calculated for each TimeStamp value:
Statistics = myRecords.DataBaseRecords
.GroupBy(x => DbFunctions.DiffHours(firstTimeStamp, x.TimeStamp) into g
.Select(g => new GraphModel
{
Date = g.FirstOrDefault().TimeStamp,
Value = g.Count()
};
If this is plain LINQ to objects (not Entity Framework) you can replace ...
DbFunctions.DiffHours(firstTimeStamp, x.TimeStamp)
... by
(x.TimeStamp - firstTimeStamp).TotalHours
If it's LINQ to SQL, use
SqlMethods.DateDiffHour(firstTimeStamp, x.TimeStamp)
Perhaps something like this may work out for you:
DateTime myDateTime = new DateTime(DateTime.Parse(firstTimeStamp).AddHours(group.Key).Ticks);
Question specific to answer above:
...Date = new DateTime(DateTime.Parse(firstTimeStamp).AddHours(group.Key))...

How to group a list of data into year, season, month, week, day?

I am dealing with big data of years.
The data model is quite simple:
public class ValueData
{
public DateTime TimeRecorded {get; set;}
public double ValueRecorded {get; set;}
}
After having a list of ValueData: List<ValueData> for years of data, I need to group the data based on: Year ==> contains data of 4 seasons: Season ==> A season contains 4 months ==> A month contains data of 4 weeks ==> A week contains data of 7 days based on the week calendar numbers of a year. Because I need to make a sum of data per year, per season, per month, per week and per day
How can I achieve this data classification? should I use LinQ?
I guess you are looking for something like conditional groups
but 2gbs of data will take a while to process.
I guess it's ok for a one time parse and save the results but if you need to run this often you'll need a more appropriate solution.
I believe you want something along the lines of the following query:
var groups = data.GroupBy(v => new {Year = v.TimeRecorded.Year,
Season = SeasonFromMonth(v.TimeRecorded.Month),
Month = v.TimeRecorded.Month,
Week = System.Globalization.CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(v.TimeRecorded, System.Globalization.CalendarWeekRule.FirstDay, System.DayOfWeek.Monday),
Day = v.TimeRecorded.Day});
With a helper function SeasonFromMonth that accepts the integer month and returns some value (probably an enum) indicating the season.
You can then select relevant subgroups with queries like:
var fallValues = groups.Where(g => g.Key.Season == Seasons.Fall);
var decemberValues = groups.Where(g => g.Key.Month == 12);
var firstOfMonth = groups.Where(g => g.Key.Day == 1);
and so on, or flatten the groups into a single list by adding a SelectMany clause (although the SelectMany will throw away the key information):
groups.Where(g => g.Key.Season == Seasons.Fall).SelectMany(g => g);
groups.Where(g => g.Key.Month == 12).SelectMany(g => g);
groups.Where(g => g.Key.Day == 1).SelectMany(g => g);

How do I add default null values in a Linq join statement

I have two IEnumerables. One contains dates, the other contains data.
DateTime start = DateTime.Today.AddDays(-21);
var dates = Enumerable.Range(0, 21).Select(n => start.AddDays(n)).ToArray();
var data = MyClass.Data.Select(x => new { Date = x.Date, Views = x.Count });
I'm trying to build a table which shows the Views on a given day. However, data contains some gaps. How do I write a linq query which joins the two sets, and returns the Views number when present or 0 when there is no matching object in data?
I can do this the old fashioned way with foreach statements but I'd like to know how to do it in Linq.
Not sure i fully understood your question.If you want to generate a list of days which has at least one views,then this will get the job done.
DateTime start = DateTime.Today.AddDays(-21);
//Sample view data
var viewsData = new[] {new {id = "id", date = new DateTime(2013, 4, 12), views = 25}};
var dates = Enumerable.Range(0, 21)
.Select(d => start.AddDays(d))
.Select(n => new
{
Day = n,
views =viewsData.Any(x => x.date == n)
? viewsData.FirstOrDefault(v => v.date == n).views
: 0
});
Zero is populated for days having no views
This should work:
var data = from day in Enumerable.Range(0, 21).Select(n => start.AddDays(n))
join d in MyClass.Data.Select(x => new { Date = x.Date, Views = x.Count })
on day equals d.Date into gj
from dd in gj.DefaultIfEmpty()
select new { Date = day, Views = dd == null ? 0 : dd.Views };
This returns the views-number when there is one at the given day and 0 otherwise.
How to: Perform Left Outer Joins
Have a look at this MSDN article: http://msdn.microsoft.com/en-us/library/vstudio/bb397895.aspx
It explains how to do a left outer join which is what you need in this case.

Categories