I have a list of Apartments where each Apartment has it's ApartmentRooms, where each ApartmentRoom has it's DateBatches. Each DateBatch has list of Dates which represents occupied dates (one record per taken date).
Apartment 1
Room 1
DateBatch
1.5.2015
2.5.2015
DateBatch 2
8.5.2015
9.5.2015
Room 2
DateBatch
5.5.2015
6.5.2015
By this, you can see that this apartment has 2 rooms of which Room 1 is taken on following dates 1.5, 2.5, 8.5 and 9.5 and room 2 is occupied on 5.5 and 6.5.
User can enter a desired period of N days and X amount of consecutive days he wants to stay.
So for an example, user enters period from 1.5 to 15.5 and he wants to sleep 10 nights, I need to list all apartments where at least one of the apartment rooms is available for any of possible date combinations, which would be following in this case:
1.5-10.5
2.5-11.5
3.5-12.5
4.5-13.5
5.5-14.5
So far I have tried this, and it works only for first foreach iteration, because foreach concatenates query with AND criteria, not OR criteria, and I think this is a very bad approach to go.
public static IQueryable<Apartment> QueryByPeriod(this IQueryable<Apartment> apartments, DateTime PeriodStart, DateTime PeriodEnd, int StayDuration)
{
var possibleDateRanges = new List<List<DateTime>>();
//set all possible start dates for the desired period
for (var i = PeriodStart; i <= PeriodEnd.AddDays(-StayDuration); i = i.AddDays(1))
{
List<DateTime> dates = new List<DateTime>();
foreach(var date in i.DateRange(i.AddDays(StayDuration-1)))
{
dates.Add(date);
}
possibleDateRanges.Add(dates);
}
//filter by date range
//select apartment rooms where one of possible combinations is suitable for selected period
foreach (var possibleDates in possibleDateRanges)
{
apartments = apartments.Where(m => m.ApartmentRooms
.Any(g => g.OccupiedDatesBatches.Select(ob => ob.OccupiedDates).Any(od => od.Any(f => possibleDates.Contains(f.Date)))
) == false);
}
return apartments;
}
Any suggestions?
To combine multiple conditions with OR, you can use (for example) LinqKit library. Install it via nuget, add using LinqKit; and then:
public static IQueryable<Apartment> QueryByPeriod(this IQueryable<Apartment> apartments, DateTime PeriodStart, DateTime PeriodEnd, int StayDuration) {
var possibleDateRanges = new List<Tuple<DateTime, DateTime>>();
// list all ranges, so for your example that would be:
//1.5-10.5
//2.5-11.5
//3.5-12.5
//4.5-13.5
//5.5-14.5
var startDate = PeriodStart;
while (startDate.AddDays(StayDuration - 1) < PeriodEnd) {
possibleDateRanges.Add(new Tuple<DateTime, DateTime>(startDate, startDate.AddDays(StayDuration - 1)));
startDate = startDate.AddDays(1);
}
Expression<Func<Apartment, bool>> condition = null;
foreach (var range in possibleDateRanges) {
Expression<Func<Apartment, bool>> rangeCondition = m => m.ApartmentRooms
// find rooms where ALL occupied dates are outside target interval
.Any(g => g.OccupiedDatesBatches.SelectMany(ob => ob.OccupiedDates).All(f => f.Date < range.Item1 || f.Date > range.Item2)
);
// concatenate with OR if necessary
if (condition == null)
condition = rangeCondition;
else
condition = condition.Or(rangeCondition);
}
if (condition == null)
return apartments;
// note AsExpandable here
return apartments.AsExpandable().Where(condition);
}
Note that I also modified your logic. Of course, this logic is perfect candidate for unit-testing, and if you are working on a serious project - you should definely test it using in-memory EF provider (or mocking) for different conditions.
Here is a pure (does not require external packages) LINQ to Entities solution.
Start with determining the list of the possible start dates:
var startDates = Enumerable.Range(0, PeriodEnd.Subtract(PeriodStart).Days - StayDuration + 1)
.Select(offset => PeriodStart.AddDays(offset))
.ToList();
Then use the following query:
var availableApartments = apartments.Where(a => a.ApartmentRooms.Any(ar =>
startDates.Any(startDate => !ar.OccupiedDatesBatches.Any(odb =>
odb.OccupiedDates.Any(od =>
od.Date >= startDate && od.Date < DbFunctions.AddDays(startDate, StayDuration))))));
The benefit of this solution is that it can easily be extended. The above query returns available apartments, but does not provide information which room is available and when - something you might need to provide to the user. Using the above approach, you can get that information like this:
public class AvailableApartmentInfo
{
public Apartment Apartment { get; set; }
public Room Room { get; set; }
public DateTime StartDate { get; set; }
}
var availableApartmentInfo =
from a in apartments
from ar in a.ApartmentRooms
from startDate in startDates
where !ar.OccupiedDatesBatches.Any(odb =>
odb.OccupiedDates.Any(od =>
od.Date >= startDate && od.Date < DbFunctions.AddDays(startDate, StayDuration)))
select new AvailableApartmentInfo { Apartment = a, Room = ar, StartDate = startDate };
Related
I have a datatable that contains three columns, I need to check when each employee ID reached two or will reach two years by subtracting Date1 from Date2 and sum the difference by a LINQ query.
If Date2 value is null that means ID is still working till now.
ID Date1 Date2
> 100 10/01/2016 09/01/2017
> 100 20/09/2017 25/05/2019
> 101 05/07/2018
I need output like below:
ID two_years
> 100 19/09/2018
> 101 04/07/2020
Using my Scan extension which is based on the APL Scan operator (like Aggregate, only it returns the intermediate results):
public static class IEnumerableExt {
// TRes seedFn(T FirstValue)
// TRes combineFn(TRes PrevResult, T CurValue)
public static IEnumerable<TRes> Scan<T, TRes>(this IEnumerable<T> src, Func<T, TRes> seedFn, Func<TRes, T, TRes> combineFn) {
using (var srce = src.GetEnumerator()) {
if (srce.MoveNext()) {
var prev = seedFn(srce.Current);
while (srce.MoveNext()) {
yield return prev;
prev = combineFn(prev, srce.Current);
}
yield return prev;
}
}
}
}
Then assuming by two years, you mean 2 * 365 days, and assuming you count the beginning and ending dates of each period as part of the total, this LINQ will find the answer:
var ans = src.Select(s => new { s.ID, s.Date1, s.Date2, Diff = (s.Date2.HasValue ? s.Date2.Value-s.Date1 : DateTime.Now.Date-s.Date1).TotalDays+1 })
.GroupBy(s => s.ID)
.Select(sg => new { ID = sg.Key, sg = sg.Scan(s => new { s.Date1, s.Date2, s.Diff, DiffAccum = s.Diff }, (res, s) => new { s.Date1, s.Date2, s.Diff, DiffAccum = res.DiffAccum + s.Diff }) })
.Select(IDsg => new { IDsg.ID, two_year_base = IDsg.sg.FirstOrDefault(s => s.DiffAccum > twoYears) ?? IDsg.sg.Last() })
.Select(s => new { s.ID, two_years = s.two_year_base.Date1.AddDays(twoYears-(s.two_year_base.DiffAccum - s.two_year_base.Diff)).Date });
If your original data is not sorted in Date1 order, or ID+Date1 order, you will need to add OrderBy to sort by Date1.
Explanation:
First we compute the days worked(?) represented by each row, using today if we don't have an ending Date2. Then group by ID and work on each group. For each ID, we compute the running sum of days worked. Next find the Date1 that proceeds the two year mark using the running sum (DiffAccum) and compute the two year date from Date1 and the remaining time needed in that period.
If it is possible a particular ID could have a lot of periods, you could use another variation of Scan, ScanUntil which short circuits evaluation based on a predicate.
I have a CSV File that i want to filter something like this
Example of my CSV File:
Name,LastName,Date
David,tod,09/09/1990
David,lopez,09/09/1994
David,cortez,09/09/1994
Maurice,perez,09/09/1980
Maurice,ruiz,09/09/1996
I want to know, How many people were born between date 1 (01/01/1990) and date 2 (01/01/1999) (with datetimepicker)
And the datagridview should it show something like this:
Name,Frecuency
David,3
Maurice,1
I dont know how do it with compare dates, but I have this code with linq logic
DataTable dtDataSource = new DataTable();
dtDataSource.Columns.Add("Name");
dtDataSource.Columns.Add("Frecuency");
int[] array = new int[10];
array[0] = 1;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 1;
array[5] = 2;
array[6] = 1;
array[7] = 1;
array[8] = 2;
array[9] = 3;
var group = from i in array
group i by i into g
select new
{
g.Key,
Sum = g.Count()
};
foreach (var g in group)
{
dtDataSource.Rows.Add(g.Key,g.Sum);
}
if (dtDataSource != null)
{
dataGridViewReporte.DataSource = dtDataSource;
}
Thanks!
The best, and easiest, way to work with dates in .NET is with the DateTimeOffset structure. This type exposes several methods for parsing dates (which makes converting the date strings from your CSV file easy), and also enables simple comparisons between dates with the standard operators.
See the DateTimeOffset documentation on MSDN.
Note: .NET also has a DateTime structure. I would encourage you to use DateTimeOffset wherever possible, as it helps prevent time zone bugs from creeping in to your code.
Simple Example
As a simple example, this code demonstrates how you can parse a string to a DateTimeOffset in .NET, and then compare it to another date.
// Static property to get the current time, in UTC.
DateTimeOffset now = DateTimeOffset.UtcNow;
string dateString = "09/09/1990";
DateTimeOffset date;
// Use TryParse to defensively parse the date.
if (DateTimeOffset.TryParse(dateString, out date))
{
// The date is valid; we can use a standard operator to compare it.
if (date < now)
{
Console.WriteLine("The parsed date is in the past.");
}
else
{
Console.WriteLine("The parsed date is in the future.");
}
}
Using LINQ
The key element you were missing from your sample code was a Where clause in the LINQ expression. Now that we've seen how to parse dates, it's simply a matter of comparing them to the start and end dates you care about.
.Where(p => p.BirthDate >= startDate && p.BirthDate <= endDate)
Note: I've found that LINQ expressions are really nice to work with when they're strongly typed to some object. I've included a simple Person class in this example, which hopefully clears up the code a lot. This should be fine for most cases, but do keep in mind that LINQ-to-Objects, while incredibly productive, is not always the most efficient solution when you have a lot of data.
The Person class:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTimeOffset BirthDate { get; set; }
}
Example code:
// Representing the CSV file as an array of strings.
var csv = new []
{
"Name,LastName,Date",
"David,tod,09/09/1990",
"David,lopez,09/09/1994",
"David,cortez,09/09/1994",
"Maurice,perez,09/09/1980",
"Maurice,ruiz,09/09/1996"
};
// Parse each line of the CSV file into a Person object, skipping the first line.
// I'm using DateTimeOffset.Parse for simplicity, but production code should
// use the .TryParse method to be defensive.
var people = csv
.Skip(1)
.Select(line =>
{
var parts = line.Split(',');
return new Person
{
FirstName = parts[0],
LastName = parts[1],
BirthDate = DateTimeOffset.Parse(parts[2]),
};
});
// Create start and end dates we can use to compare.
var startDate = new DateTimeOffset(year: 1990, month: 01, day: 01, hour: 0, minute: 0, second: 0, offset: TimeSpan.Zero);
var endDate = new DateTimeOffset(year: 1999, month: 01, day: 01, hour: 0, minute: 0, second: 0, offset: TimeSpan.Zero);
// First, we filter the people by their birth dates.
// Then, we group by their first name and project the counts.
var groups = people
.Where(p => p.BirthDate >= startDate && p.BirthDate <= endDate)
.GroupBy(p => p.FirstName)
.Select(firstNameGroup => new
{
Name = firstNameGroup.Key,
Count = firstNameGroup.Count(),
});
foreach (var group in groups)
{
dtDataSource.Rows.Add(group.Name, group.Count);
}
LINQ Syntax
As a matter of personal preference, I typically use the LINQ extension methods (.Where, .Select, .GroupBy, etc.) instead of the query syntax. Following the style from your example above, the same query could be written as:
var groups = from p in people
where p.BirthDate >= startDate && p.BirthDate <= endDate
group p by p.FirstName into g
select new
{
Name = g.Key,
Count = g.Count(),
};
I'm trying to make a Linq to SQL query that returns Date grouping results. The challenge is about grouping daily/weekly/monthly/quarterly depends of a enumerable parameter (periodicity). Below, my current Linq to SQL query:
var TotalGroupedInvoices = from c in entidades.InvoicesView
group c by (periodo == periodicity.Daily) ? c.InvoiceDate.Date :
period == periodicity.Weekly? c.InvoiceDate.Date.AddDays(-(double)c.InvoiceDate.DayOfWeek) :
period == periodicity.Monthly? new DateTime(c.InvoiceDate.Year,c.InvoiceDate.Month ,1) :
period == periodicity.Quarterly? new DateTime(c.InvoiceDate.Year, c.InvoiceDate.Month - (c.InvoiceDate.Month % 3) +1, 1) :
period == periodicity.Anual ? new DateTime(c.InvoiceDate.Year, 1, 1) : inicio into Periods
select new
{
period = Periods.Key,
Total = Periodos.Sum(c => c.Total)
};
For clarification, take a look at the quarterly period code fragment:
period == periodicity.Quarterly? new DateTime(c.InvoiceDate.Year, c.InvoiceDate.Month - (c.InvoiceDate.Month % 3) +1, 1)
Thus, for dates into first quarter like: January/12/2012, January/15/2012, or March/20/2012 i get all of them grouped at the quarter begginig: January/1/2012, so it's works as expected.
First I wonder about the query efficiency. What do you think about this? Maybe it would be better to translate periods in integers for SQL Server efficiency and re-translate to date periods on client, but i'm not sure about this.
On the other hand, the weekly group works grouping dates weekly into the first sunday of each week:
period == periodicity.Weekly? c.InvoiceDate.Date.AddDays(-(double)c.InvoiceDate.DayOfWeek)
...but that's incorrect for me because i'm from Spain and weeks start on Monday. How can i fix the week groups to take this into account?
So, summarizing:
What about this Linq to SQL query efficiency?
How can i group weekly by this but considering weeks from Monday to Sunday?
Thanks a lot!
PD: sorry for my English level.
To answer the first part, you don't really want to transmit the enum value into the databaes and let it make the decision of which code branch to use. You have all the information to make the decision locally.
Expression<Func<InvoicesView, DateTime>> groupingExpr = c => c.InvoiceDate.Date;
if (period == periodicity.Weekly)
{
groupingExpr = c => c.InvoiceDate.Date.AddDays(-(double)c.InvoiceDate.DayOfWeek);
}
else if (period == periodicity.Monthly)
{
groupingExpr = c => new DateTime(c.InvoiceDate.Year,c.InvoiceDate.Month ,1);
}
else if (period == periodicity.Quarterly)
{
groupingExpr = c => new DateTime(c.InvoiceDate.Year, c.InvoiceDate.Month - (c.InvoiceDate.Month % 3) +1, 1);
}
else if (period == periodicity.Anual
{
groupingExpr = c => new DateTime(c.InvoiceDate.Year, 1, 1);
}
var TotalGroupedInvoices = entidades.InvoicesView
.GroupBy(groupingExpr)
.Select(grouped => new {
period = grouped.Key,
Total = grouped.Sum(c => c.Total)
});
Here's the best I can do to blend the groupingExpr with query comprehension syntax:
var TotalGroupedInvoices = from grouped in entidades.InvoicesView.GroupBy(groupingExpr)
select new {
period = grouped.Key,
Total = grouped.Sum(c => c.Total)
};
To answer the second part, you just need to do some "day" arithmetic. You can do this inline or as a separate function (eg extension method):
public static DateTime FirstDayOfWeek (this DateTime date)
{
double offset = (date.DayOfWeek == DayOfWeek.Sunday) ? -6 : (DayOfWeek.Monday - date.DayOfWeek);
return date.AddDays(offset);
}
Then in your LINQ:
period == periodicity.Weekly ? c.InvoiceDate.FirstDayOfWeek
However, given that you are retrieving this from SQL you may just want to look at the date functions there eg DATEPART and return them in your queries
I am trying to build a tool that calculates something called quota based upon when employees are scheduled to work and when they request off.
My ShiftSet object is a set of Shift objects which consist of a StartTime and EndTime (both of type time(7). Each ShiftSet corresponds to a day.
ScheduleExceptions are times that an employee has off. There can be any number of overlapping or non-overlapping ScheduleExceptions in a day. They are of the datetime data type.
An example of a ShiftSet:
08:00-10:00
10:00-12:00
13:00-15:00
15:00-17:00
An example of ScheduleExceptions for that same day:
07:30-10:30
14:35-16:00
What I need to do is to find the amount of time that the employee is working on a day. The way I can figure to do this is to calculate the intersection of ShiftSet and the inverse of ScheduleExceptions.
How would I do this with time? I would prefer to use Linq if possible.
Check out this great article at CodeProject
It's probably way too broad for your specific problem but it will probably give you a good starting point on how to solve it.
As InBetween mentioned, there are libraries out there that have solved this problem, but they solve many related problems as well. If you are wanting to just tackle this particular problem without taking on another dependency, you can try the following.
// Finds ones with absolutely no overlap
var unmodified = shifts.Where(s => !exceptions.Any(e => s.Start < e.End && s.End > e.Start));
// Finds ones entirely overlapped
var overlapped = shifts.Where(s => exceptions.Any(e => e.End >= s.End && e.Start <= s.Start));
// Adjusted shifts
var adjusted = shifts.Where(s => !unmodified.Contains(s) && !overlapped.Contains(s))
.Select(s => new Shift
{
Start = exceptions.Where(e => e.Start <= s.Start && e.End > s.Start).Any() ? exceptions.Where(e => e.Start <= s.Start && e.End > s.Start).First().End : s.Start,
End = exceptions.Where(e => e.Start < s.End && e.End >= s.End).Any() ? exceptions.Where(e => e.Start < s.End && e.End >= s.End).First().Start : s.End
});
var newShiftSet = unmodified.Union(overlapped).Union(adjusted);
It's a basic example, though it could be compacted (albeit less-readable) and improved.
I didn't test bellow code, may be there is some bugs, Also I wrote it in textpad may be there are invalid characters, Idea is simple, and I try to use meaningful variables.
var orderedShifts = ShiftSets.OrderBy(x=>x.StartDate).ToList();
var compactShifts = new List<Shift>();
compactShifts.Add(orderedShifs[0]);
foreach (var item in orderedShift)
{
if (item.Start <= compactShifts[compactShifts.Count-1].End
&& item.End > compactShifts[compactShifts.Count-1].End)
{
compactShifts[compactShifts.Count-1].End = item.End;
}
else if (item.Start > compactShifts[compactShifts.Count-1].End)
compactShifts.Add(item);
}
//run similar procedure for schedule exceptions to create compact schedules.
var validShifts = new List<Shift>();
foreach (var item in compactShifts)
{
var shiftCheatingPart = compactExceptions
.FirstOrDefault(x=>x.Start < item.Start
&& x.End > item.End)
if (shiftCheatingPart != null)
{
if (item.End <= shiftCheatingPart.End)
continue;
validShifts.Add(new Shift{Start = shiftCheatingPart.End,End = item.End);
}
}
var totalTimes = validShifts.Sum(x=>x.End.Sunbtract(x.Start).TotalHours);
A very crude solution would be something like
void Main()
{
var workTime = new List<ShiftSet> {
new ShiftSet{StartTime= new TimeSpan(8,0,0),EndTime= new TimeSpan(10,0,0)},
new ShiftSet{StartTime= new TimeSpan(10,0,0),EndTime= new TimeSpan(12,0,0)},
new ShiftSet{StartTime= new TimeSpan(13,0,0),EndTime= new TimeSpan(15,0,0)},
new ShiftSet{StartTime= new TimeSpan(15,0,0),EndTime= new TimeSpan(17,0,0)}
};
var missingTime= new List<ShiftSet> {
new ShiftSet{StartTime= new TimeSpan(7,30,0),EndTime= new TimeSpan(10,30,0)},
new ShiftSet{StartTime= new TimeSpan(14,35,0),EndTime= new TimeSpan(16,0,0)}
};
Console.WriteLine(workTime.Sum(p=>p.Shift()) - missingTime.Sum(p=>p.Shift()));
}
public class ShiftSet
{
public TimeSpan StartTime {get;set;}
public TimeSpan EndTime {get;set;}
public double Shift() {return (EndTime-StartTime).TotalMinutes;}
}
I calculate a worktime in minutes so I can sum more easily using linq
I am also missing specific shift information which I think don't belong with ShiftSet class
Because the employee is not scheduled to work from 7:30 to 8:00, we would not include that time
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.