Condense event list with LINQ - c#

I have a list of log entries in Audit class
public class Audit
{
public DateTime TimeStamp { get; set; }
public string User { get; set; }
public string AuditType { get; set; }
}
so a list might look like this;
20140206 11:29:20 Owen Open
20140206 11:29:21 Owen Close
20140206 11:31:20 Owen Open
20140206 11:32:20 Owen Close
20140206 11:42:20 Owen Open
20140206 11:50:00 Owen Acknowledge
This gives us gaps of 1 second, 1 minute, and 40 seconds. So the longest time it was open was the middle pair for 1 minute, then it was acknowledged at 11:50. I'm looking for the date pair where it was open longes, in this case 1 min.
I know I can process the list in sequentially and find the biggest gap using a TimeSpan but I figure there is a neat LINQ way to do it maybe with groups?
UPDATE It's not pretty, but this is the logic in really expanded walk
var audits = notice.AuditEntries.Where(a => a.User == user);
DateTime? currentOpen = null;
DateTime? bestOpen = null;
DateTime? bestClose = null;
foreach (var audit in audits)
{
if (audit.AuditType == "Open")
{
if (currentOpen.HasValue) continue;
currentOpen = audit.TimeStamp;
}
if (audit.AuditType == "Close" || audit.AuditType == "Acknowledge")
{
if (currentOpen.HasValue)
{
DateTime? currentClose = audit.TimeStamp;
if (!bestOpen.HasValue)
{
bestOpen = currentOpen;
bestClose = currentClose;
}
else
{
if (bestClose.Value.Subtract(bestOpen.Value) > currentClose.Value.Subtract(currentOpen.Value))
{
bestOpen = currentOpen;
bestClose = currentClose;
}
}
currentOpen = null;
}
}
}

I think this will do the trick:
IEnumerable<Audit> audits = ...
var longestAuditsByUser = audits.OrderBy(a => a.Timestamp)
// group by user, since presumably we don't want to match an open from one user with a close from another
.GroupBy(a => a.User)
.Select(userAudits =>
{
// first, align each audit entry with it's index within the entries for the user
var indexedAudits = userAudits.Select((audit, index) => new { audit, index });
// create separate sequences for open and close/ack entries
var starts = indexedAudits.Where(t => t.audit.AuditType == "Open");
var ends = indexedAudits.Where(t => t.audit.AuditType == "Close" || t.audit.AuditType == "Acknowledge");
// find the "transactions" by joining starts to ends where start.index = end.index - 1
var pairings = starts.Join(ends, s => s.index, e => e.index - 1, (start, end) => new { start, end });
// find the longest such pairing with Max(). This will throw if no pairings were
// found. If that can happen, consider changing this Select() to SelectMany()
// and returning pairings.OrderByDescending(time).Take(1)
var longestPairingTime = pairings.Max(t => t.end.Timestamp - t.start.Timestamp);
return new { user = userAudits.Key, time = longestPairingTime };
});
// now that we've found the longest time for each user, we can easily find the longest
// overall time as well
var longestOverall = longestAuditsByUser.Max(t => t.time);

Not tested but should work:
var auditGaps = audits
.GroupBy(a => a.User)
.Select(g => new
{
User = g.Key,
MinOpen = g.Where(a => a.AuditType == "Open").Select(a=> a.TimeStamp).Min(),
MaxClosed = g.Where(a => a.AuditType == "Close").Select(a=> a.TimeStamp).Max(),
MaxAcknowledge = g.Where(a => a.AuditType == "Acknowledge").Select(a=> a.TimeStamp).Max()
})
.Select(x => new
{
x.User,
LargestOpenCloseGap = x.MaxClosed - x.MinOpen,
LargestOpenAcknowledgeGap = x.MaxAcknowledge - x.MinOpen
});

Related

In a LINQ with a select can I compare forward to the next row and decide what to select?

What I implemented with a for loop is this:
phraseSources2 = new List<PhraseSource2>();
for (int i = 0; i < phraseSources.Count; i++)
{
var ps = phraseSources[i];
if (i != phraseSources.Count - 1)
{
var psNext = phraseSources[i + 1];
if (psNext != null &&
ps.Kanji == psNext.Kanji &&
ps.Kana == psNext.Kana &&
ps.English.Length <= psNext.English.Length)
{
i++;
ps = phraseSources[i];
}
} else
{
ps = phraseSources[i];
}
phraseSources2.Add(new PhraseSource2()
{
Kanji = ps.Kanji,
Kana = ps.Kana,
Furigana = ps.Furigana,
English = ps.English,
});
}
Previously I had been using LINQ
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Select(x => new PhraseSource2()
{
Kanji = x.Kanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
I know LINQ can do a lot but can it look forward at the next row when doing a select?
If I understand your problem correcly I wouldn't "look forward" but use GroupBy instead and group by Kanji and Kana then Select the longest English as the value in the PhraseSource2 object.
Something like this:
var phraseSource2 = phraseSources
.GroupBy(x => new {Kanji = x.Kanji, Kana = x.Kana})
.Select(g => new PhraseSource2 {
Kanji = g.Key.Kanji,
Kana = g.Key.Kana,
Furigana = g.First().Furigana,
English = g.OrderByDescending(x => x.English.Length).First().English
});
If the source collection can be accessed by index than you can use an overload to the select which gives you the current index.
var source = new[] { 'a', 'b', 'c' };
var result = source.Select((x, i) => new { Current = x, Next = source.Length > i+1 ? source[i+1] : ' '});
All you have to do is just set up a variable inside a query where you can easily retrieve next or previous value like this:
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Select((x, y) =>
var NextKanji = (List<Data1.Model.PhraseSource2>)phraseSources.Skip(y + 1).FirstOrDefault().Kanji;
new PhraseSource2()
{
Kanji = NextKanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
If you want to check some conditions before, you can do it like this:
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Where((x, y) =>
var NextEnglish = (List<Data1.Model.PhraseSource2>)phraseSources.Skip(y + 1).FirstOrDefault().English;
x.English.Length < NextEnglish.Length)
.Select(x =>
new PhraseSource2()
{
Kanji = x.Kanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
There is no built-in method, but there are third-party libraries that offer this functionality. The MoreLinq is a respected and free .NET library that offers a WindowLeft extension method, that processes a sequence into a series of subsequences representing a windowed subset of the original. So you could use it to process your phraseSources in pairs, and discard the pairs that have two equal phrases. Finally select the first phrase of the pairs that survived.
using static MoreLinq.Extensions.WindowLeftExtension;
var phraseSources2 = phraseSources
.WindowLeft(size: 2)
.Where(phrases => // phrases is of type IList<PhraseSource2>
{
if (phrases.Count == 2) // All have size 2 except from the last
{
var ps = phrases[0];
var psNext = phrases[1];
return ps.Kanji != psNext.Kanji || ps.Kana != psNext.Kana ||
ps.English.Length > psNext.English.Length;
}
else // The last is a single phrase
{
return true;
}
})
.Select(window => window[0]) // Select the first phrase
.ToList();

C# Ordering event dates with null values last

I'm working on a course listing in C# and an course can have up to 5 dates of when they are running. Ideally, the next date after today in the future would be selected, and ordered accordingly in a list.
What i have so far is a course list that gets the next date, and displays it, but it displays all the events without dates first (Null/Blank). I'm trying to show the courses with next dates first, and then those without after this.
C# Code:
public ActionResult FilterList(string role = null, string category = null)
{
return View("~/Views/FilterList.cshtml", GetCourses(role, category));
}
[NonAction]
public List<IEnumerable<Course>> GetCourses(string role = null, string category = null)
{
var collection = new List<IEnumerable<Course>>();
var items = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CourseRoot)
.Children.Where(m => m.TemplateID == Course.TemplateID)
.Select(m => (Course)m).ToList();
var dates = new List<FilterDates>();
items.ForEach(m => dates.Add(new FilterDates
{
Dates = new List<DateTime>{ m.Date1, m.Date2, m.Date3, m.Date4, m.Date5 },
Name = m.Name
}));
dates.ForEach(m => m.Dates.RemoveAll(n => n == new DateTime(0001, 01, 01)));
dates.ForEach(m => m.Dates.Sort((a, b) => a.CompareTo(b)));
dates = dates.OrderBy(m => m.Dates.AsQueryable().FirstOrDefault(n => n - DateTime.Now >= TimeSpan.Zero)).ToList();
var model = new List<Course>();
dates.ForEach(m => model.Add(items.AsQueryable().FirstOrDefault(n => n.Name == m.Name)));
if (!string.IsNullOrEmpty(role) || !string.IsNullOrEmpty(category))
{
var currentRole = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CategoryRoot)
.Children.AsQueryable().FirstOrDefault(m => m.Fields["Key"].Value == role);
if (!string.IsNullOrEmpty(category))
{
var currentCategory = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.SeriesRoot)
.Children.AsQueryable().FirstOrDefault(m => m.Fields["Key"].Value == category);
model = model.Where(m => m.Series == currentCategory.Name).ToList();
if (string.IsNullOrEmpty(role))
{
collection.Add(model);
}
}
if (!string.IsNullOrEmpty(role))
{
model = model.Where(m => m.InnerItem.Children.Where(n => n.Fields["Key"].Value == currentRole.Name).Any()).ToList();
List<Course> required = new List<Course>(), recommended = new List<Course>(), refresh = new List<Course>();
foreach (var item in model)
{
foreach (Item inner in item.InnerItem.Children)
{
if (inner.Fields["Key"].Value == currentRole.Name)
{
switch (inner.Fields["Severity"].Value)
{
case "Required":
required.Add(item);
break;
case "Recommended":
recommended.Add(item);
break;
case "Refresh":
refresh.Add(item);
break;
}
}
}
}
collection.Add(required);
collection.Add(recommended);
collection.Add(refresh);
}
}
else
{
collection.Add(model);
}
return collection;
}
I've tried different orderbys, but can't seem to get the ordering right. Any help would be greatly appreciated.
Andy
The code you posted has some extra stuff that seems unrelated to your question about sorting. I am ignoring that and just addressing the question at hand: how to sort your courses so that the ones with the nearest future date are first.
I would create a little method to return the next future date or DateTime.MaxValue as the "null" value.
private DateTime GetNextFutureDate(Course course)
{
var dates =
new[] {course.Date1, course.Date2, course.Date3, course.Date4, course.Date5}.Where(d => d > DateTime.Now).ToArray();
return dates.Length == 0 ? DateTime.MaxValue : dates[0];
}
Then in your GetCourses method you could use it like this:
[NonAction]
public List<IEnumerable<Course>> GetCourses(string role = null, string category = null)
{
var collection = new List<IEnumerable<Course>>();
var model = Sitecore.Context.Database.GetItem(SitecoreIDs.Pages.CourseRoot)
.Children.Where(m => m.TemplateID == Course.TemplateID)
.Select(m => (Course)m).OrderBy(m => GetNextFutureDate(m));
if (!string.IsNullOrEmpty(role) || !string.IsNullOrEmpty(category))
// ... the rest of your code ...
return collection;
}
You might also want to consider making GetNextFutureDate a member or extension method on your Course class.

Merge two or more T in List<T> based on condition

I have the below class:
public class FactoryOrder
{
public string Text { get; set; }
public int OrderNo { get; set; }
}
and collection holding the list of FactoryOrders
List<FactoryOrder>()
here is the sample data
FactoryOrder("Apple",20)
FactoryOrder("Orange",21)
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes",71)
FactoryOrder("mango",72)
FactoryOrder("Cherry",73)
My requirement is to merge the Text of FactoryOrders where orderNo are in sequence and retain the lower orderNo for the merged FactoryOrder
- so the resulting output will be
FactoryOrder("Apple Orange",20) //Merged Apple and Orange and retained Lower OrderNo 20
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes mango Cherry",71)//Merged Grapes,Mango,cherry and retained Lower OrderNo 71
I am new to Linq so not sure how to go about this. Any help or pointers would be appreciated
As commented, if your logic depends on consecutive items so heavily LINQ is not the easiest appoach. Use a simple loop.
You could order them first with LINQ: orders.OrderBy(x => x.OrderNo )
var consecutiveOrdernoGroups = new List<List<FactoryOrder>> { new List<FactoryOrder>() };
FactoryOrder lastOrder = null;
foreach (FactoryOrder order in orders.OrderBy(o => o.OrderNo))
{
if (lastOrder == null || lastOrder.OrderNo == order.OrderNo - 1)
consecutiveOrdernoGroups.Last().Add(order);
else
consecutiveOrdernoGroups.Add(new List<FactoryOrder> { order });
lastOrder = order;
}
Now you just need to build the list of FactoryOrder with the joined names for every group. This is where LINQ and String.Join can come in handy:
orders = consecutiveOrdernoGroups
.Select(list => new FactoryOrder
{
Text = String.Join(" ", list.Select(o => o.Text)),
OrderNo = list.First().OrderNo // is the minimum number
})
.ToList();
Result with your sample:
I'm not sure this can be done using a single comprehensible LINQ expression. What would work is a simple enumeration:
private static IEnumerable<FactoryOrder> Merge(IEnumerable<FactoryOrder> orders)
{
var enumerator = orders.OrderBy(x => x.OrderNo).GetEnumerator();
FactoryOrder previousOrder = null;
FactoryOrder mergedOrder = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (mergedOrder == null)
{
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
else
{
if (current.OrderNo == previousOrder.OrderNo + 1)
{
mergedOrder.Text += current.Text;
}
else
{
yield return mergedOrder;
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
}
previousOrder = current;
}
if (mergedOrder != null)
yield return mergedOrder;
}
This assumes FactoryOrder has a constructor accepting Text and OrderNo.
Linq implementation using side effects:
var groupId = 0;
var previous = Int32.MinValue;
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.Select(x =>
{
var #group = x.OrderNo != previous + 1 ? (groupId = x.OrderNo) : groupId;
previous = x.OrderNo;
return new
{
GroupId = group,
Item = x
};
})
.GroupBy(x => x.GroupId)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Item.Text).ToArray()),
x.Key))
.ToArray();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
output:
Apple Orange 20
WaterMelon 42
JackFruit 51
Grapes mango Cherry 71
Or, eliminate the side effects by using a generator extension method
public static class IEnumerableExtensions
{
public static IEnumerable<IList<T>> MakeSets<T>(this IEnumerable<T> items, Func<T, T, bool> areInSameGroup)
{
var result = new List<T>();
foreach (var item in items)
{
if (!result.Any() || areInSameGroup(result[result.Count - 1], item))
{
result.Add(item);
continue;
}
yield return result;
result = new List<T> { item };
}
if (result.Any())
{
yield return result;
}
}
}
and your implementation becomes
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.MakeSets((prev, next) => next.OrderNo == prev.OrderNo + 1)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Text).ToArray()),
x.First().OrderNo))
.ToList();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
The output is the same but the code is easier to follow and maintain.
LINQ + sequential processing = Aggregate.
It's not said though that using Aggregate is always the best option. Sequential processing in a for(each) loop usually makes for better readable code (see Tim's answer). Anyway, here's a pure LINQ solution.
It loops through the orders and first collects them in a dictionary having the first Id of consecutive orders as Key, and a collection of orders as Value. Then it produces a result using string.Join:
Class:
class FactoryOrder
{
public FactoryOrder(int id, string name)
{
this.Id = id;
this.Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
The program:
IEnumerable<FactoryOrder> orders =
new[]
{
new FactoryOrder(20, "Apple"),
new FactoryOrder(21, "Orange"),
new FactoryOrder(22, "Pear"),
new FactoryOrder(42, "WaterMelon"),
new FactoryOrder(51, "JackFruit"),
new FactoryOrder(71, "Grapes"),
new FactoryOrder(72, "Mango"),
new FactoryOrder(73, "Cherry"),
};
var result = orders.OrderBy(t => t.Id).Aggregate(new Dictionary<int, List<FactoryOrder>>(),
(dir, curr) =>
{
var prevId = dir.SelectMany(d => d.Value.Select(v => v.Id))
.OrderBy(i => i).DefaultIfEmpty(-1)
.LastOrDefault();
var newKey = dir.Select(d => d.Key).OrderBy(i => i).LastOrDefault();
if (prevId == -1 || curr.Id - prevId > 1)
{
newKey = curr.Id;
}
if (!dir.ContainsKey(newKey))
{
dir[newKey] = new List<FactoryOrder>();
}
dir[newKey].Add(curr);
return dir;
}, c => c)
.Select(t => new
{
t.Key,
Items = string.Join(" ", t.Value.Select(v => v.Name))
}).ToList();
As you see, it's not really straightforward what happens here, and chances are that it performs badly when there are "many" items, because the growing dictionary is accessed over and over again.
Which is a long-winded way to say: don't use Aggregate.
Just coded a method, it's compact and quite good in terms of performance :
static List<FactoryOrder> MergeValues(List<FactoryOrder> dirtyList)
{
FactoryOrder[] temp1 = dirtyList.ToArray();
int index = -1;
for (int i = 1; i < temp1.Length; i++)
{
if (temp1[i].OrderNo - temp1[i - 1].OrderNo != 1) { index = -1; continue; }
if(index == -1 ) index = dirtyList.IndexOf(temp1[i - 1]);
dirtyList[index].Text += " " + temp1[i].Text;
dirtyList.Remove(temp1[i]);
}
return dirtyList;
}

Efficient LINQ to Entities query

I have an entity collection of Readings.
Each Reading is linked to an entity called Meter.
(And each Meter holds multiple readings).
each Reading holds a field for meter id (int) and a field for time.
Here is some simplified code to demonstrate it:
public class Reading
{
int Id;
int meterId;
DateTime time;
}
public class Meter
{
int id;
ICollection<Readings> readings;
}
Given a specific period and list of meterids,
what would be the most efficient way to get for each Meter
the first and last reading in that time period?
I am able to iterate through all meters and for each meter to obatin
first and last reading for the period,
but I was wandering if there is a more efficient way to acheive this.
And a bonus question: same question, but with multiple periods of time to get data for,
instead of just one period.
I am not exactly sure how you want this data, but you could project it into an anonymous type:
var metersFirstAndLastReading = meters.Select(m => new
{
Meter = m,
FirstReading = m.readings.OrderBy(r => r.time).First(),
LastReading = m.readings.OrderBy(r => r.time).Last()
});
You can then read your result list like this (this example is just meant as an illustration):
foreach(var currentReading in metersFirstAndLastReading)
{
string printReadings = String.Format("Meter id {0}, First = {1}, Last = {2}",
currentReading.Meter.id.ToString(),
currentReading.FirstReading.time.ToString(),
currentReading.LastReading.time.ToString());
// Do something...
}
Another option would be to create properties in Meter which dynamically return the first and last readings:
public class Meter
{
public int id;
public List<Reading> readings;
public Reading FirstReading
{
get
{
return readings.OrderBy(r => r.time).First();
}
}
public Reading LastReading
{
get
{
return readings.OrderBy(r => r.time).Last();
}
}
}
EDIT: I misunderstood the question a little.
Here is the implementation to determine the first and last readings for a meter including a date range (assuming meterIdList is an ICollection<int> of IDs and begin and end is the specified date range)
var metersFirstAndLastReading = meters
.Where(m => meterIdList.Contains(m.id))
.Select(m => new
{
Meter = m,
FirstReading = m.readings
.Where(r => r.time >= begin && r.time <= end)
.OrderBy(r => r.time)
.FirstOrDefault(),
LastReading = m.readings
.Where(r => r.time >= begin && r.time <= end)
.OrderByDescending(r => r.time)
.FirstOrDefault()
});
You won't be able to use properties now (as you need to supply parameters) so methods will work just fine as an alternative:
public class Meter
{
public int id;
public List<Reading> readings;
public Reading GetFirstReading(DateTime begin, DateTime end)
{
var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end);
if(!HasReadings(begin, end))
{
throw new ArgumentOutOfRangeException("No readings available during this period");
}
return filteredReadings.OrderBy(r => r.time).First();
}
public Reading GetLastReading(DateTime begin, DateTime end)
{
var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end);
if(!HasReadings(begin, end))
{
throw new ArgumentOutOfRangeException("No readings available during this period");
}
return filteredReadings.OrderBy(r => r.time).Last();
}
public bool HasReadings(DateTime begin, DateTime end)
{
return readings.Any(r => r.time >= begin && r.time <= end);
}
}
I have a very similar data model where this code is used to get the oldest readings, i just changed it to also include the newest.
I use query syntax to do something like this:
var query = from reading in db.Readings
group reading by reading.meterId
into readingsPerMeter
let oldestReadingPerMeter = readingsPerMeter.Min(g => g.time)
let newestReadingPerMeter = readingsPerMeter.Max(g => g.time)
from reading in readingsPerMeter
where reading.time == oldestReadingPerMeter || reading.time == newestReadingPerMeter
select reading; //returns IQueryable<Reading>
That would result in a only the newest and oldest reading for each meter.
The reason i think this is efficient is because its one lookup to the DB to get all the readings for each meter, instead of several lookups for each meter. We have ~40000 meters with ~30mil readings. i just tested the lookup on our data it took about 10s
The sql preformed is a crossjoin between two sub selects for each of the min and max dates.
UPDATE:
Since this is queryable you should be able to supply a period after, like this:
query.Where(r=>r.time > someTime1 && r.time < someTime2)
Or put it into the original query, i just like it seperated like this. The query isnt executed yet since we havent performed an action that fetches the data yet.
Create a new class as the return type called Result, which looks like this
public class Result
{
public int MeterId;
public Readings Start;
public Readings Last;
}
I emulated your situation by making a list of Meters and populating some data, your query should be pretty much the same though
var reads = Meters.Where(x => x.readings != null)
.Select(x => new Result
{
MeterId = x.id,
Start = x.readings.Select(readings => readings).OrderBy(readings=>readings.time).FirstOrDefault(),
Last = x.readings.Select(readings=>readings).OrderByDescending(readings=>readings.time).FirstOrDefault()
});
public IEnumerable<Reading> GetFirstAndLastInPeriod
(IEnumerable<Reading> readings, DateTime begin, DateTime end)
{
return
from reading in readings
let span = readings.Where(item => item.time >= begin && item.time <= end)
where reading.time == span.Max(item => item.time)
|| reading.time == span.Min(item => item.time)
select reading;
}
meters.Where(mt=>desiredMeters.Contains(mt)).Select(mt=>
new{
mt.Id,
First = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).FirstOrDefault(),
Last = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).LastOrDefault()
});
If you have lots of readings per meter, this will not perform well, and you should consider Readings to be of SortedList class.
my solution will return exact what u want (List of all Meters containing Readings within given Time Period)
public IList<Reading[]> GetFirstAndLastReadings(List<Meter> meterList, DateTime start, DateTime end)
{
IList<Reading[]> fAndlReadingsList = new List<Reading[]>();
meterList.ForEach(x => x.readings.ForEach(y =>
{
var readingList = new List<Reading>();
if (y.time >= startTime && y.time <= endTime)
{
readingList.Add(y);
fAndlReadingsList.Add(new Reading[] { readingList.OrderBy(reading => reading.time).First(), readingList.OrderBy(reading => reading.time).Last() });
}
}));
return fAndlReadingsList;
}
I got some very nice leads, thank to all the responders.
Here is the solution that worked for me:
/// <summary>
/// Fills the result data with meter readings matching the filters.
/// only take first and last reading for each meter in period.
/// </summary>
/// <param name="intervals">time intervals</param>
/// <param name="meterIds">list of meter ids.</param>
/// <param name="result">foreach meter id , a list of relevant meter readings</param>
private void AddFirstLastReadings(List<RangeFilter<DateTime>> intervals, List<int> meterIds, Dictionary<int, List<MeterReading>> result)
{
foreach (RangeFilter<DateTime> interval in intervals)
{
var metersFirstAndLastReading = m_context.Meter.Where(m => meterIds.Contains(m.Id)).Select(m => new
{
MeterId = m.Id,
FirstReading = m.MeterReading
.Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal)
.OrderBy(r => r.TimeStampLocal)
.FirstOrDefault(),
LastReading = m.MeterReading
.Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal)
.OrderByDescending(r => r.TimeStampLocal)
.FirstOrDefault()
});
foreach (var firstLast in metersFirstAndLastReading)
{
MeterReading firstReading = firstLast.FirstReading;
MeterReading lastReading = firstLast.LastReading;
if (firstReading != null)
{
result[firstLast.MeterId].Add(firstReading);
}
if (lastReading != null && lastReading != firstReading)
{
result[firstLast.MeterId].Add(lastReading);
}
}
}
}
}

linq-to-sql group by with count and custom object model

I'm looking to fill an object model with the count of a linq-to-sql query that groups by its key.
The object model looks somewhat like this:
public class MyCountModel()
{
int CountSomeByte1 { get; set; }
int CountSomeByte2 { get; set; }
int CountSomeByte3 { get; set; }
int CountSomeByte4 { get; set; }
int CountSomeByte5 { get; set; }
int CountSomeByte6 { get; set; }
}
This is what I have for the query:
var TheQuery = from x in MyDC.TheTable
where ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7
group x by x.SomeByte into TheCount
select new MyCountModel()
{
CountSomeByte1 = TheCount.Where(TheCount => TheCount.Key == 1)
.Select(TheCount).Count(),
CountSomeByte2 = TheCount.Where(TheCount => TheCount.Key == 2)
.Select(TheCount).Count(),
.....
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
}.Single();
ListOfRecordIDs is list of longs that's passed in as a parameter. All the CountSomeByteN are underlined red. How do you do a count of grouped elements with the group's key mapped to an object model?
Thanks for your suggestions.
The select is taking each element of your group and projecting them to identical newly created MyCountModels, and you're only using one of them. Here's how I'd do it:
var dict = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count());
var result = new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
}
EDIT: Here's one way to do it in one statement. It uses an extension method called Into, which basically works as x.Into(f) == f(x). In this context, it can be viewed as like a Select that works on the whole enumerable rather than on its members. I find it handy for eliminating temporary variables in this sort of situation, and if I were to write this in one statement, it's probably how I'd do it:
public static U Into<T, U>(this T self, Func<T, U> func)
{
return func(self);
}
var result = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count())
.Into(dict => new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
});
Your range variable is not correct in the subqueries:
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
In method notation you don't need the extra select:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Count(),
If you want to use it anyway:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Select(theCount => theCount).Count(),

Categories

Resources