I'm currently working on an application in ASP.NET MVC 4.5. I need to write a LINQ query to order a list of Projects by different StatusIds.
Given is a list of Projects with this ViewModel:
public class ProjectVm
{
public int ProjectId { get; set; }
public string Title { get; set; }
public Status StatusId { get; set; }
}
My project status enum:
public enum Status : byte
{
Draft = 1,
Pending = 2,
Validated = 3,
Refused = 4
}
The idea is to bring the List<ProjectVm> in a certain order, thus first ordered by 1 Draft, second by 2 Pending, third by 4 Refused and fourth by 3 Validated.
My current query looks like this:
projects = projects.OrderBy(x => x.StatusId).ToList();
Unfortunately this query doesn't respect the desired order (4 comes before 3).
Do you know how to apply a condition on this query to bring the projects into the right order (1, 2, 4, 3) ?
Thanks!
Just use several orderings, the first one with OrderByDescending, then the rest with ThenByDescending:
projects = projects
.OrderByDescending(p => p.StatusId == Status.Draft)
.ThenByDescending(p => p.StatusId == Status.Pending)
.ThenByDescending(p => p.StatusId == Status.Refused)
.ThenByDescending(p => p.StatusId == Status.Validated)
.ToList();
There's not a clean way to do it completely inline - you could do something like:
projects.OrderBy(x => x == Status.Validated ? int.MaxValue : (int)x.StatusId)
to force Validated to be at the end, but I would write a function:
private int CustomOrder(Status status)
{
switch(status)
{
// force Validated to the end
case Status.Validated:
return int.MaxValue;
default:
return (int)status;
}
}
and call it from the query:
projects.OrderBy(x => CustomOrder(x))
Since you can add comments and organize the code to make it clearer what your intent is.
Another option would be to put the values in array in the order you want, then order by their position in the array:
Status[] order = new [] {Draft, Pending, Refused, Validated};
projects.OrderBy(x => Array.IndexOf(order,x));
Try this:
public static int MyCustomOrder (Status status)
{
switch (status)
{
case Status.Draft : return 1;
case Status.Pending : return 2;
case Status.Validated : return 4;
case Status.Refused : return 3;
default: return -1;
}
}
And now:
var result = projects.OrderBy (x => MyCustomOrder (x.StatusId));
Not really pretty, but should work:
projects.OrderBy(x => x.StatusId).ThenBy(c => c.StatusId == Status.Validated ? 1 : 0).ToList();
Otherwise you need to provide your own Comparer:
class StatusComparer : IComparer<Status>
{
public int Compare(Status x, Status y)
{
if (x.Equals(y)) return 0;
return (x > y || x.Equals(Status.Validated)) ? 1 : -1;
}
}
And then call:
projects.OrderBy(x => x.StatusId, new StatusComparer()).ToList();
Or do something like the other people here proposed ;)
projects.OrderBy(x => x.StatusId == Status.Validated).ThenBy(x => x.StatusId)
Puts all the pending at the end, then sorts within that rule by StatusID. Two simple operations and likely to be processed well by any provider.
projects.OrderBy(x => x.StatusId == Status.Validated ? int.MaxValue : (int)x.StatusId)
A single operation, which is hence likely faster, that re-assigns the 3 for Pending to int.MaxValue before the sort.
I'd try the second first as the likely more efficient, but the second is worth noting as a general approach too.
Try this
var projectList= projects.OrderBy(x => (int)x.StatusId).ToList();
Related
I have a Linq query where I am getting response values to a set of questions. I group the response data by question, and then perform various aggregations on the responsedata, such as averaging them. I also count the proportion of responses that could be classified as "high", "middle" and "low", based on specifying response option value ranges.
var result = ItemResponses
.Where (ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.Select (ir => ir)
.GroupBy (ir => new {
ir.ItemID
}).Select (grouped => new {
ItemID = grouped.Key.ItemID,
Average = (double)grouped.Average (g => g.OptionValue),
ProportionHighScore =
(double)grouped.Where(g => g.OptionValue == 5 || g.OptionValue == 6).Count()
/ (double)grouped.Count(),
});
I would like to move the code where I specify which optionvalues should be combined into a "high" response category away from the Linq query, and was considering to set up an extension method to do this. In the extension method I can then specify different combinations of response option values that I can score into a "high" score, across scenarious (for example, if the max response was not 6 but 10, then I would count options 8, 9, and 10 towards a "high" response category.
The extension method might look something like this:
public static double ProportionHighScore(this IGrouping<a,b> values, int ResponseOptionID)
{
double ret = 0;
switch (ResponseOptionID)
{
case 1:
//code here to combine response options 5 and 6, and divide by total
break;
case 2:
//code here to combine response options 8, 9 and 10 and divide by total
break;
case 3:
//etc..
break;
default:
break;
}
return ret;
}
But the question I have then is: how to I go about passing the Linq grouping values as a parameter into the extension method? The type of the IGrouping b is an anonymous type.
Update:
I like the idea of just doing GroupBy (ir => it.ItemID) so that I get access to "IGrouping<int, ItemResponse>". But in the code here I simplified a bit. In my actual code there are a few more things going on, such as reversing the OptionValue scores if an item is flagged as "IsReversed".
var result2 = ItemResponses
.Where (ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.Select (ir => new {
ItemID = ir.ItemID,
OptionValue =
(
//Reverse option value of items that are flagged to require reverse scoring
ir.Item.IsReversed == 0 ? ir.OptionValue :
((ir.ResponseScaleOption.ResponseScale.MaxValue + 1) - ir.OptionValue)
),
})
.GroupBy (g => new {g.ItemID})
.Select (grouped => new {
ItemID = grouped.Key.ItemID,
Average = (double)grouped.Average (g => g.OptionValue),
ProportionHighScore =
(double)grouped.Where(g => g.OptionValue == 5 || g.OptionValue == 6).Count()
/ (double)grouped.Count(),
});
In some versions of this query I also include fields from some joined tables as well. So the need to reverse the OptionValues is one reason why I assumed I needed an anonymous type. Perhaps I need to create a new class that I can project into ("ItemResponseForAggregation", or some such name), and then be able to do IGrouping<int, ItemResponseForAggregation> for my extension parameter?
You can't pass anonymous types in this way. They have to be in the immediate execution context to treat them as if they were strong types. Create a lightweight type so you can pass this in, and then add a parameter constraint on parameter 'b' to enforce that it must be of the type you've created.
I did a mock up in LINQPad of what I think you're looking for (if I am understanding the requirements correctly):
void Main()
{
var ItemResponses = new List<ItemResponse>();
var result = ItemResponses
.Where(ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.GroupBy(ir => ir.ItemID)
.Select(
grouped => new {
ItemID = grouped.Key,
Average = (double)grouped.Average(g => g.OptionValue),
ProportionHighScore = grouped.ProportionHighScore(1, 2, 3, 4, 5)
}
);
result.Dump();
}
public class ItemResponse
{
public int ItemID { get; set; }
public int OptionValue { get; set; }
public AssessmentInstanceItem AssessmentInstance { get; set; }
}
public class AssessmentInstanceItem
{
public int ProjectID { get; set; }
}
public static class ItemResponseExtensions
{
public static double ProportionHighScore(this IGrouping<int, ItemResponse> values, params int[] ResponseOptionID)
{
double count = 0;
double total = values.Count();
foreach (int r in ResponseOptionID)
count += (double)values.Where(g => g.OptionValue == r).Count();
return count / total;
}
}
In the extension method, the params parameter allows you to specify as many options as you need. Then I am just looping through the options, adding the count for each response option.
I implemented this comparer which works OK.
class ReservationDatesDistinctComparer : IEqualityComparer<ReservationModel>
{
public bool Equals(ReservationModel x, ReservationModel y)
{
return x.FromDate.Date== y.FromDate.Date && x.ToDate.Date == y.ToDate.Date && x.UnitId == x.UnitId;
}
public int GetHashCode(ReservationModel product)
{
int hashProductCode = 1;
return hashProductCode;
}
}
But on ReservationModel I have some other property let's call it ReservationType and I would like to filter out with distinct same dates but keep only ReservationModel who has Type A not Type B.
How it is posible to affect on Distinct which model it will choose?
Distinct will keep the elements it encounters first, a possible solution would be to order those which have ReservationType A first:
reservatonModels.OrderByDescending(m => m.ReservationType == ReservationType.A)
.Distinct(new ReservationDatesDistinctComparer());
I don't think you can use Distinct for this. (Unless you want to rely on undocumented implementation details, as per Lukazoid's answer.)
Something similar to this might do the trick. (Group the elements that your comparer deems to be equal, then order each group so that Type A is prioritised, then take the first element from each group.)
var result = source.GroupBy(x => x, new ReservationDatesDistinctComparer())
.Select(g => g.OrderBy(x => (x.ReservationType == "Type A") ? 1 : 2)
.First());
I'm fairly new to programming but have been tasked with maintaining some applications that were created by a previous employee. I have a ?: statement that now needs to handle more than a true or false statement but I'm not sure how to go about doing it. The code in question is:
MailDomainContext mail = new MailDomainContext();
mail.Load(mail.GetMailsQuery("Workforce Attendence Issue",
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().Username,
(loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().EmployeeShiftID >= 2 ? "supervisor1" : "supervisor2"),
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().FirstName,
attendence.AttendenceDate.ToString("MM/dd/yyyy"),
attendence.TimeLost,
loadAbs.Entities.Where(abs => abs.AbsenceID == attendence.AbsenceID).First().AbsenceDescription,
(from inf in loadAtt.Entities
where inf.EmployeeID == _EmployeeID
where inf.AttendenceDate > DateTime.Now.AddDays(30 * -1)
where inf.Approved == false
select inf).Count() + 1,
attendence.UTOUsed
), null, null);
More specifically this line:
(loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().EmployeeShiftID >= 2 ? "supervisor1" : "supervisor2"),
I need to add 4 more supervisors to the list but haven't figured out a way to do it that doesn't make everything else unhappy. I apologize if this is too simple a question or if I left out some details you might need to know, as I said I'm pretty new to all of this.
This code is needlessly hard to maintain and also inefficient and not very defensive. The code is retrieving the employee three times.
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().Username
The above line (and others) will throw an exception if the employee with _EmployeeID doesn't exist. Instead, you could use FirstOrDefault, or SingleOrDefault if you expect there to only ever be one employee with that ID (which should be the case as it looks like primary key for that entity). If loadEmp is actually an Entity Framework DbContext then you could also use Find.
You can do this query once and store the result in a local variable.
var employee = loadEmp.Entities.SingleOrDefault(emp => emp.EmployeeID == _EmployeeID);
if (employee == null)
{
// Handle employee not found
}
To then get the supervisor string based on the employee, you could create a method which takes the minimum amount of information needed to calculate the supervisor string, and pass this into the method to get your result.
GetSupervisorRole(employee.EmployeeShiftID);
...
private string GetSupervisorRole(int employeeShiftID)
{
// Logic here
}
One approach is to extract that code into a method and write that method any way you want.
Another approach is to use dictionary to map keys (if you have small number of them) to values.
var id =3;
var mapping = new Dictionary<int, string>() {
{ 1, "first" },
{ 2, "second" },
{ 3, "first" } //you can map 2 values (1,3) to the same "first" string
};
string value;
if (!mapping.TryGetValue(id, out value))
{
value = "unknown";
}
Create the following method:
string GetSupervisor(int employeeShiftId) {
if (employeeShiftId == 1) supervisor = "supervisor1";
else if (employeeShiftId == 2) supervisor = "supervisor2";
else if (employeeShiftId == 3) supervisor = "supervisor3";
else if (employeeShiftId == 4) supervisor = "supervisor4";
}
Then call it from your code and assign the result to a variable supervisor, which you can then use in mail.Load():
int employeeShiftId = loadEmp.Entities
.Where(emp => emp.EmployeeID == _EmployeeID).First()
.EmployeeShiftID;
string supervisor = GetSupervisor(employeeShiftId);
MailDomainContext mail = new MailDomainContext();
mail.Load(mail.GetMailsQuery("Workforce Attendence Issue",
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().Username,
supervisor, // <-- Then use it here
...
);
I would replace this whole section
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().Username,
(loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().EmployeeShiftID >= 2 ? "supervisor1" : "supervisor2"),
loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First().FirstName,
with
//Assign these variables ahead of time so others reading your code can
//figure out what's going on
var EmpID = loadEmp.Entities.Where(emp => emp.EmployeeID == _EmployeeID).First();
var UserName = EmpID.UserName;
var FirstName = EmpID.FirstName;
var Title = GetTitle(EmpID.EmployeeShiftID);
//Your original call
Mail.Load(mail.GetMailsQuery("Workforce Attendence Issue",
UserName,
Title,
FirstName,
//...Etc
);
//New method you will need to add, you could do this logic in line, but this will
//be less messy
private string GetTitle(int ShiftID)
{
switch (ShiftID)
{
case 1:
return "supervisor1";
break;
case 2:
return "supervisor2";
break;
//...etc
}
}
Not that this is a great idea, but you can combine inline if's as so:
static void Main(string[] args)
{
int EmpId = 2;
string supervisor = EmpId == 4 ? "Supervisor4" :
EmpId == 3 ? "Supervisor3" :
EmpId == 2 ? "supervisor2" :
"supervisor1" ;
Console.WriteLine(supervisor);
}
You can see another example of this in another stack question: Legible or not: C# multiple ternary operators + Throw if unmatched
I would probably instead go for the Method approach like KDiTraglia proposed where you just pass in the EmpId and get the supervisor name back, or alternatively the Dictionary lookup approach like Alexei Levenkov propsed though.
I have the following LINQ method that works as expected except if there are No Rows Found then I get a Null Exception. I am struggling on how I modify this to return 0 if that occurs.
public static int GetLastInvoiceNumber(int empNumber)
{
using (var context = new CmoDataContext(Settings.Default.LaCrosse_CMOConnectionString))
{
context.Log = Console.Out;
IQueryable<tblGreenSheet> tGreenSheet = context.GetTable<tblGreenSheet>();
return (tGreenSheet
.Where(gs => gs.InvoiceNumber.Substring(2, 4) == empNumber.ToString())
.DefaultIfEmpty()
.Max(gs => Convert.ToInt32(gs.InvoiceNumber.Substring(6, gs.InvoiceNumber.Length)))
);
}
}
Thanks
I tried one of Jon Skeet's suggestions, below, and now I get Unsupported overload used for query operator 'DefaultIfEmpty'
public static int GetLastInvoiceNumber(int empNumber)
{
using (var context = new CmoDataContext(Settings.Default.LaCrosse_CMOConnectionString))
{
context.Log = Console.Out;
IQueryable<tblGreenSheet> tGreenSheet = context.GetTable<tblGreenSheet>();
return tGreenSheet
.Where(gs => gs.InvoiceNumber.Substring(2, 4) == empNumber.ToString())
.Select(gs => Convert.ToInt32(gs.InvoiceNumber.Substring(6, gs.InvoiceNumber.Length)))
.DefaultIfEmpty(0)
.Max();
}
}
You're using
.Where(...)
.DefaultIfEmpty()
which means if there are no results, pretend it's a sequence with a single null result. You're then trying to use that null result in the Max call...
You can probably change it to:
return tGreenSheet.Where(gs => ...)
.Max(gs => (int?) Convert.ToInt32(...)) ?? 0;
This uses the overload finding the maximum of int? values - and it returns an int? null if there were no values. The ?? 0 then converts that null value to 0. At least, that's the LINQ to Objects behaviour... you'll have to check whether it gives the same result for you.
Of course, you don't need to use the ?? 0 if you're happy to change the method signature to return int? instead. That would give extra information, in that the caller could then tell the difference between "no data" and "some data with a maximum value of 0":
return tGreenSheet.Where(gs => ...)
.Max(gs => (int?) Convert.ToInt32(...));
Another option is to use the overload of DefaultIfEmpty() which takes a value - like this:
return tGreenSheet.Where(gs => ...)
.Select(gs => Convert.ToInt32(...))
.DefaultIfEmpty(0)
.Max();
In situations like this when there may or may not be a matching item, I prefer to return an object rather than a value type. If you return a value type, you have to have some semantics about what value means "there is nothing here." I would change it to return the last invoice, then (when it is non-null) get the invoice number from the invoice. Add a method to the class to return the numeric invoice number from the string.
public static tbleGreenSheet GetLastInvoice(int empNumber)
{
using (var context = new CmoDataContext(Settings.Default.LaCrosse_CMOConnectionString))
{
context.Log = Console.Out;
return context.GetTable<tblGreenSheet>()
.Where(gs => gs.InvoiceNumber.Substring(2, 4) == empNumber.ToString())
.OrderByDescending(gs => Convert.ToInt32(gs.InvoiceNumber.Substring(6, gs.InvoiceNumber.Length)))
.FirstOrDefault();
}
}
public class tbleGreenSheet
{
....
public int NumericInvoice
{
get { return Convert.ToInt32(InvoiceNumber.Substring(6, InvoiceNumber.Length)); }
}
...
}
Used as
var invoice = Foo.GetLastInvoice( 32 );
if (invoice != null)
{
var invoiceNumber = invoice.NumericInvoice;
...do something...
}
else
{
...do something else...
}
I had a remarkably similar experience with IQueryable<T> and NHibernate. My solution:
public static TExpr MaxOrDefault<TItem, TExpr>(this IQueryable<TItem> query,
Expression<Func<TItem, TExpr>> expression) {
return query.OrderByDescending(expression).Select(expression).FirstOrDefault();
}
The only drawback is that you are stuck with the standard default value, instead of getting to specify one.
I'm trying to select a subgroup of a list where items have contiguous dates, e.g.
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
1 41 Meeting with John 03/06/2010
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
5 41 Meeting with Kay 14/06/2010
6 41 Meeting Continues 15/06/2010
I'm using a pivot point each time, so take the example pivot item as 3, I'd like to get the following resulting contiguous events around the pivot:
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
My current implementation is a laborious "walk" into the past, then into the future, to build the list:
var activity = // item number 3: Meeting Continues (09/06/2010)
var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray();
// Walk into the past until a gap is found
var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID);
DateTime dayBefore;
var previousEvent = activity;
while (previousEvent != null)
{
dayBefore = previousEvent.ActivityDate.AddDays(-1).Date;
previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault();
if (previousEvent != null)
{
if (previousEvent.ActivityDate.Date == dayBefore)
relatedActivities.Insert(0, previousEvent);
else
previousEvent = null;
}
}
// Walk into the future until a gap is found
var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
DateTime dayAfter;
var nextEvent = activity;
while (nextEvent != null)
{
dayAfter = nextEvent.ActivityDate.AddDays(1).Date;
nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault();
if (nextEvent != null)
{
if (nextEvent.ActivityDate.Date == dayAfter)
relatedActivities.Add(nextEvent);
else
nextEvent = null;
}
}
The list relatedActivities should then contain the contiguous events, in order.
Is there a better way (maybe using LINQ) for this?
I had an idea of using .Aggregate() but couldn't think how to get the aggregate to break out when it finds a gap in the sequence.
Here's an implementation:
public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
this IEnumerable<T> source,
Func<T, int> keySelector
)
{
int keyGroup = Int32.MinValue;
int currentGroupValue = Int32.MinValue;
return source
.Select(t => new {obj = t, key = keySelector(t))
.OrderBy(x => x.key)
.GroupBy(x => {
if (currentGroupValue + 1 < x.key)
{
keyGroup = x.key;
}
currentGroupValue = x.key;
return keyGroup;
}, x => x.obj);
}
You can either convert the dates to ints by means of subtraction, or imagine a DateTime version (easily).
In this case I think that a standard foreach loop is probably more readable than a LINQ query:
var relatedActivities = new List<TActivity>();
bool found = false;
foreach (var item in activities.OrderBy(a => a.ActivityDate))
{
int count = relatedActivities.Count;
if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
{
if (found)
break;
relatedActivities.Clear();
}
relatedActivities.Add(item);
if (item.ID == activity.ID)
found = true;
}
if (!found)
relatedActivities.Clear();
For what it's worth, here's a roughly equivalent -- and far less readable -- LINQ query:
var relatedActivities = activities
.OrderBy(x => x.ActivityDate)
.Aggregate
(
new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
(a, x) =>
{
if (a.ShortCircuit)
return a;
int count = a.List.Count;
if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
{
if (a.Found)
return new { a.List, a.Found, ShortCircuit = true };
a.List.Clear();
}
a.List.Add(x);
return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
},
a => a.Found ? a.List : new List<TActivity>()
);
Somehow, I don't think LINQ was truly meant to be used for bidirectional-one-dimensional-depth-first-searches, but I constructed a working LINQ using Aggregate. For this example I'm going to use a List instead of an array. Also, I'm going to use Activity to refer to whatever class you are storing the data in. Replace it with whatever is appropriate for your code.
Before we even start, we need a small function to handle something. List.Add(T) returns null, but we want to be able to accumulate in a list and return the new list for this aggregate function. So all you need is a simple function like the following.
private List<T> ListWithAdd<T>(List<T> src, T obj)
{
src.Add(obj);
return src;
}
First, we get the sorted list of all activities, and then initialize the list of related activities. This initial list will contain the target activity only, to start.
List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList();
List<Activity> relatedActivities = new List<Activity>();
relatedActivities.Add(activity);
We have to break this into two lists, the past and the future just like you currently do it.
We'll start with the past, the construction should look mostly familiar. Then we'll aggregate all of it into relatedActivities. This uses the ListWithAdd function we wrote earlier. You could condense it into one line and skip declaring previousEvents as its own variable, but I kept it separate for this example.
var previousEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID).Reverse();
relatedActivities = previousEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, prevItem) => items.OrderBy(a => a.ActivityDate).First().ActivityDate.Subtract(prevItem.ActivityDate).Days.Equals(1) ? ListWithAdd(items, prevItem) : items).ToList();
Next, we'll build the following events in a similar fashion, and likewise aggregate it.
var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList();
You can properly sort the result afterwards, as now relatedActivities should contain all activities with no gaps. It won't immediately break when it hits the first gap, no, but I don't think you can literally break out of a LINQ. So it instead just ignores anything which it finds past a gap.
Note that this example code only operates on the actual difference in time. Your example output seems to imply that you need some other comparison factors, but this should be enough to get you started. Just add the necessary logic to the date subtraction comparison in both entries.