Render a Calendar with Timeslots and Overlapping Appointments - c#

I'm struggling to get my head round this one.
I have a List<Appointment> which contains DateTime Start and DateTime End along with a few others bits.
I want to draw a day planner which lists all of the appointments in order with placeholders/gaps where appropriate.
I have managed this, but now I need to handle the scenario where appointments overlap. It seems this could be;
appointment a starts before and ends after appointment b
appointment a starts before and ends during appointment b
appointment a starts during and end after appointment b
appointment a start during and ends during appointment b
This feels like a problem which must have been tackled before. Any pointers?
Here's an example of some horrendous code i'm currently working on;
List<TechActivityModel> techActivity = new TechService().GetTechActivity(7, new DateTime(2018, 4, 11), new DateTime(2018, 4, 11).AddDays(1))
.OrderBy(t => t.Start).ToList();
for (int i = techActivity.Count - 1; i >= 0; i--)
{
//something starts before and ends after
TechActivityModel clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].End).FirstOrDefault();
while (clash != null)
{
//split the clashing activity into a task before and another task after the existing on
//first create the 2nd part of the task (after the existing one)
TechActivityModel activityContinued = new TechActivityModel()
{
Start = techActivity[i].End,
End = clash.End,
Subject = "++" + clash.Subject
};
activityContinued.Length = (int)(activityContinued.End - activityContinued.Start).TotalMinutes;
techActivity.Add(activityContinued);
//update the clashing task to finish when the existing task starts
clash.Subject = "+" + clash.Subject;
clash.End = techActivity[i].Start;
clash.Length = (int)(clash.End - clash.Start).TotalMinutes;
clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].End).FirstOrDefault();
}
}
for (int i = techActivity.Count - 1; i >= 0; i--)
{
//something starts before and ends during
TechActivityModel clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start <= techActivity[i].Start && t.End > techActivity[i].Start).FirstOrDefault();
while (clash != null)
{
//update the clashing task to finish when the existing task starts
clash.Subject = "/" + clash.Subject;
clash.End = techActivity[i].Start;
clash.Length = (int)(clash.End - clash.Start).TotalMinutes;
clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].Start).FirstOrDefault();
}
}
techActivity = techActivity.OrderBy(t => t.Start).ToList();
//now we're going to pad all the gaps
List<TechActivityModel> newList = new List<TechActivityModel>();
DateTime LastEnd = techActivity[0].End;
//start with the gap from midnight till the first task
newList.Add(new TechActivityModel()
{
Start = new DateTime(2018, 4, 10),
End = techActivity[0].Start,
TicketNumber = 0,
Note = "",
TicketSubject = "",
TimeLogged = 0,
Id = 0
}
);
//pad all the gaps
for (int i = 1; i < techActivity.Count; i++)
{
if (LastEnd < techActivity[i].Start.AddMinutes(-2))
{
TechActivityModel gap = new TechActivityModel()
{
Start = LastEnd.AddMinutes(1),
End = techActivity[i].Start,
Subject = "",
Id = 0
};
gap.Length = (int)(gap.End - gap.Start).TotalMinutes;
newList.Add(gap);
}
LastEnd = techActivity[i].End;
}
//and finally fill the gap from the last task till midnight
newList.Add(new TechActivityModel()
{
Start = LastEnd,
End = new DateTime(2018, 4, 11),
Subject = "",
Length = 0
}
);
newList.AddRange(techActivity);
string content = "";
foreach (TechActivityModel techActivityModel in newList.OrderBy(t => t.Start))
{
content +=
techActivityModel.Start.ToShortTimeString()
+ " - " + techActivityModel.End.ToShortTimeString()
+ " (" + techActivityModel.Length + "mins)"
+ " : " + techActivityModel.Subject
+ Environment.NewLine;
}

Here's how I'd approach you problem, if I understand you correctly. I'd start by finding all of the distinct Start and End values, all together in a single list. We want them distinct and in order. Then we zip that list with itself to generate pairs:
var l = new List<Appointment>();
var ex = l.SelectMany(a => new[] { a.Start, a.End }).Distinct().OrderBy(dt => dt);
var pairs = ex.Zip(ex.Skip(1), (First, Second) => new { First, Second });
Now, using the rule I mentioned in the comments:
Two time periods overlap if period a starts before period b ends and period b starts before period a ends.
We work through each pair of datetimes and re-query our appointments list for all appointments that overlap the period described by the pair. If you get no results, this time period is currently free. If you get one result, you have (for this period) an appointment that's not overlapped by anything else. If you get multiple results, you have a clash and can work out how to display that. But note that you don't have to keep revisiting this time period and extending/shortening it, updating its text, etc.

Related

How to remove a range from a list within a loop?

Greetings Please keep in mind there is no database and these are fake functions to make the component work for testing, i have a List which makes a 24 hours based on 15 minutes scale and produces from this method:
public List<ScaleLine> GetHoursAndScales(ScaleLine.Scales scale = ScaleLine.Scales.Fiftheen)
{
int _scale = Convert.ToInt32(scale);
int _count = _scale * 24;
int _scaleCount = 60 / _scale;
List<ScaleLine> _list = new List<ScaleLine>();
var start = DateTime.Today;
var clockQuery = from offset in Enumerable.Range(1, _count)
select TimeSpan.FromMinutes(_scaleCount * offset);
foreach (var time in clockQuery)
{
_list.Add(new ScaleLine() { Id = _list.Count, Hours = (start + time).ToString("HH:mm"), Scale = _scale });
}
return _list;
}
And i have another list which is called Reserved hours which is produces on this method:
public List<Reservedhours> AllReservedHours()
{
return new List<Reservedhours>
{
new Reservedhours() { Id = 1, Date = DateTime.Now, StartPoint = "08:00", EndPoint = "10:00" },
new Reservedhours() { Id = 2, Date = DateTime.Now, StartPoint = "14:00", EndPoint = "16:00" },
new Reservedhours() { Id = 3, Date = DateTime.Now, StartPoint = "20:00", EndPoint = "22:00" },
new Reservedhours() { Id = 4, Date = DateTime.Now.AddDays(1), StartPoint = "07:00", EndPoint = "11:00" },
new Reservedhours() { Id = 5, Date = DateTime.Now.AddDays(1), StartPoint = "13:00", EndPoint = "15:00" },
new Reservedhours() { Id = 6, Date = DateTime.Now.AddDays(1), StartPoint = "15:00", EndPoint = "18:00" },
new Reservedhours() { Id = 7, Date = DateTime.Now.AddDays(1), StartPoint = "18:00", EndPoint = "22:00" },
};
}
Now i have another list that produces the available hours based on reserved hours and first list that produces 24 hours:
public List<ScaleLine> GetAvailableHours(DateTime date, ScaleLine.Scales scale = ScaleLine.Scales.Fiftheen)
{
List<Reservedhours> _timeLine = AllReservedHours().Where(x => x.Date.Date == date.Date)
.Select( a => new Reservedhours {StartPoint = a.StartPoint, EndPoint = a.EndPoint } )
.ToList();
List<ScaleLine> _available = GetHoursAndScales();
//scale convert:
int _scale = Convert.ToInt32(scale);
foreach (var _item in _timeLine)
{
int index = _available.Where(x => x.Hours == _item.StartPoint)
.SingleOrDefault().Id;
//Convert to datetime
DateTime _opening = DateTime.ParseExact(_item.StartPoint, "HH:mm", System.Globalization.CultureInfo.InvariantCulture);
DateTime _closing = DateTime.ParseExact(_item.EndPoint, "HH:mm", System.Globalization.CultureInfo.InvariantCulture);
//Getting duration time
TimeSpan duration = _closing.Subtract(_opening);
double _duration = duration.TotalMinutes;
//getting coverage
int timeScale = 60 / _scale;
int coverage = Convert.ToInt32(_duration) / timeScale;
//remove unavailable timespots
_available.RemoveRange(index, coverage);
}
return _available;
}
But problem is when the Foreach loop starts it removes the first range correctly based on its index, lets say if _available list has 96 members it removes 8 of them, so second time it should have 88 members and find the index within those 88 members but it doesn't and gets the index wrong (it takes the index as if the list still had 96 members) and so goes for every other action within the loop. how can i fix this issue? is there a way i can get the available list without doing a foreach loop?
The problem is your determination of the index. Instead of asking the list for the index of the desired object, you ask for the property value of an object and using this as index:
int index = _available.Where(x => x.Hours == _item.StartPoint)
.SingleOrDefault()
.Id;
Either you ask really for the index by calling IndexOf():
var matchingItem = _available.Where(x => x.Hours == _item.StartPoint)
.First();
var index = _available.IndexOf(matchingItem);
Or you replace your .RemoveRange() by something else, that really removes your desired elements.

LINQ Where clause inline for loop?

I have database values called start and length.
start is the start time of a booking (1->09:00, 2->10:00 etc) and length is the length in hours.
i then have an array of start times and end times. I want to be able to check whether each start and end pair are already booked. I so far have it figured that if the start times are the same, it is booked, or if the end times are the same, it is also booked. But if the start and end time are inbetween the comparison times, it will return not booked, which is false.
I am trying to write a LINQ query to test whether a booking is already in the database. So far I have
var proposedRequest = db.requests.Include(r => r.rooms);
proposedRequest = proposedRequest.Where(r => r.booked.Equals(1));
proposedRequest = proposedRequest.Where(r => r.roundID.Equals(roundID));
proposedRequest = proposedRequest.Where(r => r.day.Equals(day));
int[] startTimes = new int[length];
int[] endTimes = new int[length];
for(var q=0;q<length;q++)
{
startTimes[q] = time + q;
endTimes[q] = time + q + 1;
}
proposedRequest = proposedRequest.Where(s => startTimes.Contains(s.start) || endTimes.Contains(s.start+s.length));
Now, this only works for if the new booking starts at the same time as the booking already in the DB, or if it ends at the same time. This doesn't look at the following case
there is a records in the db where start -> 2 and length ->3.
so this booking runs from 10:00->13:00.
but say I am checking this against an entry that starts at 11:00 and ends at 12. It would not come back as booked already because the start and end times do not match.
What is the best way to solve this?
the only way i could see fit is to loop through my startTime and endTime arrays and have another clause for each pair that would produce something like the following:
.Where((s => s.startTime<startTime[i] && (s.startTime + s.Length) > endTime[i]) || (s => s.startTime<startTime[i+1] && (s.startTime + s.Length) > endTime[i+1]))
but i dont think this is possible.
Based on this answer, two ranges overlap if (StartA <= EndB) and (EndA >= StartB)
In your case:
StartA = s.start
EndA = s.start + s.length
StartB = time
EndB = time + length
So your last condition should be like this:
proposedRequest = proposedRequest.Where(s => s.start <= time + length &&
s.start + s.length >= time);
This will return objects that have your StartTime, EndTime, and a boolean that signifies if it booked already.
var proposedRequest = db.requests
.Include(r => r.rooms)
.Where(r => r.booked.Equals(1))
.Where(r => r.roundID.Equals(roundID))
.Where(r => r.day.Equals(day))
.ToList();
//int[] startTimes = new int[length];
//int[] endTimes = new int[length];
//for(var q=0;q<length;q++)
//{
// startTimes[q] = time + q;
// endTimes[q] = time + q + 1;
//}
var times=Enumerable
.Range(time,length)
.Select(r=>
new {
StartTime=r,
EndTime=r+1,
Booked=proposedRequest.Any(pr=>pr.StartTime<=r && pr.StartTime+pr.Length>r)
}).ToList();

How to remove first element from linq query

Im trying to consolidate a list of records of in and out times per day to the minimum number of records possible.
What i have done so far, is grouped up the lines into the groups they need to be in, and put the in and out times in a list for each day.
then i want to process the lists and add the first set of in and out lines onto a single line, then process the next entry and either create a new line or fill in the blanks of the previous line.
The bit im stuck with is removing the first item from the linq result after i have processed it.
happy to look at doing it a different way.
here is what i have:
List<LoginRecordLine> condensedLoginRecordLines = new List<LoginRecordLine>();
List<LoginRecordLine> currentLoginRecordLines = GetLoginRecordsForLoginRecordReport(lowerDate, upperDate, sageDatabaseID, loggedInUserID);
var groupedLines = from LoginRecordLine line in currentLoginRecordLines
group line by new { line.TimesheetID, line.WorkPatternPayRateID } into g
select new
{
Lines = g,
TimesheetID = g.Key.TimesheetID,
PayRateID = g.Key.WorkPatternPayRateID
};
foreach (var g in groupedLines)
{
var monTimes = from line in g.Lines
orderby line.MonTimeIn ascending
where line.MonTimeSpan != TimeSpan.Zero
select new
{
TimeIn = line.MonTimeIn,
TimeOut = line.MonTimeOut,
Timesheet = line.Timesheet,
PayRate = line.WorkPatternPayRate
};
var tueTimes = //Same as monday
var wedTimes = //Same as monday
var thuTimes = //same as monday
var friTimes = //same as monday
var satTimes = //same as monday
var sunTimes = //same as monday
while (monTimes.Count() != 0 || tueTimes.Count() != 0 || wedTimes.Count() != 0 || thuTimes.Count() != 0 || friTimes.Count() != 0 || satTimes.Count() != 0 || sunTimes.Count() != 0)
{
LoginRecordLine condensedLine = new LoginRecordLine();
if (monTimes.Count() >0)
{
condensedLine.MonTimeIn = monTimes.First().TimeIn;
condensedLine.MonTimeOut = monTimes.First().TimeOut;
condensedLine.Timesheet = monTimes.First().Timesheet;
condensedLine.WorkPatternPayRate = monTimes.First().PayRate;
//*************** REVELANT PART *************/
//remove first item from monday list
}
// tue
// wed
// etc
}
}
return condensedLoginRecordLines;
Update - Working code - before performance changes
List<LoginRecordLine> condensedLoginRecordLines = new List<LoginRecordLine>();
List<LoginRecordLine> currentLoginRecordLines = GetLoginRecordsForLoginRecordReport(lowerDate, upperDate, sageDatabaseID, loggedInUserID);
var groupedLines = from LoginRecordLine line in currentLoginRecordLines
group line by new { line.TimesheetID, line.WorkPatternPayRateID } into g
select new
{
Lines = g,
TimesheetID = g.Key.TimesheetID,
PayRateID = g.Key.WorkPatternPayRateID
};
foreach (var g in groupedLines)
{
var monTimes = (from line in g.Lines
orderby line.MonTimeIn ascending
where line.MonTimeSpan != TimeSpan.Zero
select new
{
TimeIn = line.MonTimeIn,
TimeOut = line.MonTimeOut,
Timesheet = line.Timesheet,
PayRate = line.WorkPatternPayRate
}).ToList();
var tueTimes = //Same as monday
var wedTimes = //Same as monday
var thuTimes = //same as monday
var friTimes = //same as monday
var satTimes = //same as monday
var sunTimes = //same as monday
while (monTimes.Count != 0 || tueTimes.Count != 0 || wedTimes.Count != 0 || thuTimes.Count != 0 || friTimes.Count != 0 || satTimes.Count != 0 || sunTimes.Count != 0)
{
LoginRecordLine condensedLine = new LoginRecordLine();
if (monTimes.Count >0)
{
condensedLine.MonTimeIn = monTimes.First().TimeIn;
condensedLine.MonTimeOut = monTimes.First().TimeOut;
condensedLine.Timesheet = monTimes.First().Timesheet;
condensedLine.WorkPatternPayRate = monTimes.First().PayRate;
condensedLoginRecordLines.Add(condensedLine);
monTimes.RemoveAt(0);
}
//etc
}
}
return condensedLoginRecordLines;
use the List.RemoveAt Method something like myList.RemoveAt(0) will remove the first item of your list
You should revise your algorithm and maybe data structures.
For anonymous types in queries I would add a DayOfWeek property, so the queries will look like:
var monTimes = from line in g.Lines
orderby line.MonTimeIn ascending
where line.MonTimeSpan != TimeSpan.Zero
select new
{
TimeIn = line.MonTimeIn,
TimeOut = line.MonTimeOut,
Timesheet = line.Timesheet,
PayRate = line.WorkPatternPayRate,
WeekDay = DayOfWeek.Monday
};
Then, the final loop will be replaced by something like:
var condensedLoginRecordLines = monTimes
.Concat(tueTimes)
.Concat(wedTimes)
..//etc
.Select(data => new CondensedLine { WeekDay = data.WeekDay, /* here all the properties are initialized */ })
.ToList();
And that's all.
If you still prefer to use the MonInTime, TueInTime, etc. properties, move the creation of CondensedLine into a separate function which applies a switch on the WeekDay and initializes the relevant properties only. In this case you should declare a private class instead the anonymous types you currently use in order to pass the information from method to another.
I came across a similar problem. In my case, the source data was a CSV file parsed into a string and I wanted to remove the header information before processing. I could have used the approach of casting to a list and removing the first entry, but I discovered that it is possible to add a second parameter to the where query that would give you the row number index. This works with both IQueryable and IEnumerable overloads as well.
public static List<string> GetDataLinesFromCSV(string csv)
{
var csvLines = csv.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var dataLines = csvLines.AsQueryable().Where((x, idx) => idx > 0).ToList();
return dataLines;
}

C# - Grouping Rows with dates into Blocks with a time duration

This is a little hard to explain. I have a datatable with schedule information. Each row represents a schedule with a start date/time and an end date/time. I need to group these such that the overall start to end time matches a given duration.
For example, I might have the following in my datatable:
Schedule1: Start - 9:00AM, End - 9:30AM
Schedule2: Start - 9:30AM, End - 10:00AM
Schedule3: Start - 10:00AM, End - 10:30AM
Schedule4: Start - 10:30AM, End - 11:00AM
Now if I'm given a duration value of 60 min, then I need to be able to produce the following as output:
Block1: Schedules(1,2): 9:00AM - 10:00AM
Block2: Schedules(2,3): 9:30AM - 10:30AM
Block3: Schedules(3,4): 10:00AM - 11:00AM
If however the duration was instead 120 min, then I would need to produce the following:
Block1: Schedules(1,2,3,4): 9:00AM - 11:00AM
Let me know if this needs clarification. I need to write a method in C# to do this conversion. Please help me with this as I've been stuck on it for a long time.
Whether you choose to do this in C# or SQL depends partly on the scale of the data. Assuming that we're working with a relatively small number of time ranges (say < 10), it would be reasonable to pull all the times into memory and find the blocks in C#.
Given the following classes:
public class Schedule {
public int ID { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int Minutes { get; set; }
}
public class ScheduleBlock : Schedule {
public List<Schedule> Schedules { get; set; }
}
Here is a simple algorithm that iteratively combines ranges together until every possible combination is represented (note that the number of combinations grows as O(n^2)):
public List<ScheduleBlock> CombineAllSchedules(List<Schedule> origschedules, out int added)
{
added = 0;
var schedules = new List<ScheduleBlock>();
foreach (var s in origschedules) {
var snew = new ScheduleBlock { Schedules = new List<Schedule> { s }, Start = s.Start, End = s.End, Minutes = s.Minutes };
schedules.Add(snew);
}
for (var i = 0; i < schedules.Count; i++) {
var s = schedules[i];
var matchstart = schedules.Where (s2 => s2.End == s.Start).ToList();
var matchend = schedules.Where (s2 => s2.Start == s.End).ToList();
foreach (var s2 in matchstart) {
var newschedule = CombineSchedules(s2, s);
if (!schedules.Any (sc => sc.Start == newschedule.Start && sc.End == newschedule.End)) {
schedules.Add(newschedule);
added++;
}
}
foreach (var s2 in matchend) {
var newschedule = CombineSchedules(s, s2);
if (!schedules.Any (sc => sc.Start == newschedule.Start && sc.End == newschedule.End)) {
schedules.Add(newschedule);
added++;
}
}
}
return schedules;
}
public ScheduleBlock CombineSchedules(Schedule s1, Schedule s2)
{
var schedules = new List<Schedule>();
if (s1 is ScheduleBlock) schedules.AddRange(((ScheduleBlock)s1).Schedules);
else schedules.Add(s1);
if (s2 is ScheduleBlock) schedules.AddRange(((ScheduleBlock)s2).Schedules);
else schedules.Add(s2);
var s = new ScheduleBlock {
Schedules = schedules,
Start = s1.Start, End = s2.End, Minutes = s1.Minutes + s2.Minutes
};
return s;
}
Once the combinations are put together, then it is a simple matter to query them and get specific lengths (like 60 minutes or 120 minutes):
public List<ScheduleBlock> FindBlocks(List<Schedule> schedules, int blockLength)
{
int added;
var combinedSchedules = CombineAllSchedules(schedules, out added);
var result = combinedSchedules.Where (s => s.Minutes == blockLength).ToList();
return result;
}
With this algorithm in place, you can do something like this for example to get the output you're looking for:
var schedules = new List<Schedule> {
new Schedule { ID = 1, Start = DateTime.Parse("09:00 AM"), End = DateTime.Parse("09:30 AM") },
new Schedule { ID = 2, Start = DateTime.Parse("09:30 AM"), End = DateTime.Parse("10:00 AM") },
new Schedule { ID = 3, Start = DateTime.Parse("10:00 AM"), End = DateTime.Parse("10:30 AM") },
new Schedule { ID = 4, Start = DateTime.Parse("10:30 AM"), End = DateTime.Parse("11:00 AM") },
};
foreach (var s in schedules) {
s.Minutes = (int)(s.End - s.Start).TotalMinutes;
}
Console.WriteLine("60 Minute Blocks");
Console.WriteLine("----------------");
var blocks = FindBlocks(schedules, 60);
var blockId = 1;
foreach (var block in blocks) {
var output = "Block" + blockId +
": Schedules(" + string.Join(",", block.Schedules.Select (s => s.ID)) + "): " +
block.Start.ToString("h:mmtt") + " - " + block.End.ToString("h:mmtt");
Console.WriteLine(output);
blockId++;
}
Console.WriteLine();
Console.WriteLine("120 Minute Blocks");
Console.WriteLine("----------------");
blocks = FindBlocks(schedules, 120);
blockId = 1;
foreach (var block in blocks) {
var output = "Block" + blockId +
": Schedules(" + string.Join(",", block.Schedules.Select (s => s.ID)) + "): " +
block.Start.ToString("h:mmtt") + " - " + block.End.ToString("h:mmtt");
Console.WriteLine(output);
blockId++;
}
Sample Result:
60 Minute Blocks
----------------
Block1: Schedules(1,2): 9:00AM - 10:00AM
Block2: Schedules(2,3): 9:30AM - 10:30AM
Block3: Schedules(3,4): 10:00AM - 11:00AM
120 Minute Blocks
----------------
Block1: Schedules(1,2,3,4): 9:00AM - 11:00AM

LINQ get objects by date interval

I'm looking for a LINQ query that will select only those objects whose date interval is not higher than 20 seconds. For example:
AuthenticationEssay[] essays = new AuthenticationEssay[] {
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(20), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(24), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(29), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(38), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(125), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(347), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(400), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(422), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(446), Success = false },
new AuthenticationEssay() { Date = DateTime.Now.AddSeconds(467), Success = false }
};
I want to select only the first occurence of those objects whose date interval is not longer than 20 seconds against the next object. In this case, the query should return only the first 4 objects. Any idea? :(
UPDATE
Sorry, I forgot to mention that I'm sorting the array by descending order. So yes, the position in the array shouldn't have any effect on the query.
What about this?
var query from i in Enumerable.Range(1, count - 1)
let current = list[i]
let previous = list[i - 1]
// I see some empty positions in your example, nullability check
where current != null && previous != null
where (current.Date - previous.Date).TotalSeconds < 20
select previous;
EDIT: Obviously you have to call First() in order to get only the first element of the sequence.
query.First();
EDIT 2: I have just read you are ordering your results descending. In this case the query will be slightly different:
var query from i in Enumerable.Range(1, count - 1)
let current = list[i]
let previous = list[i - 1]
// I see some empty positions in your example, nullability check
where current != null && previous != null
where (previous.Date - current.Date).TotalSeconds < 20
select current;
It ain't pretty, but here you go...
var result = Enumerable.Range(0, essays.Count() - 1)
.Select(i => new {Essays1 = essays[i], Essays2 = essays[i + 1]})
.Where(a => a.Essays2 != null)
.Where(a => a.Essays2.Date - a.Essays1.Date < new TimeSpan(0, 0, 0, 20))
.Select(a => a.Essays1);
Does it have to be LINQ? I love LINQ, but I think something like this would be more readable...
var result = new List<AuthenticationEssay>();
for (var i = 0; i < (essays.Count() - 1); i++)
{
if (essays[i + 1] != null)
if (essays[i + 1].Date - essays[i].Date < new TimeSpan(0, 0, 0, 20))
result.Add(essays[i]);
}
It is probably possible to do it using built-in Linq operators, but in this case I think writing a specific method is easier. You could do something like that:
static IEnumerable<AuthenticationEssay> Filter(IEnumerable<AuthenticationEssay> list)
{
AuthenticationEssay last = null;
AuthenticationEssay previous = null;
foreach(var item in list)
{
if (last == null)
{
// Always return the first item
yield return item;
}
else if ((item.Date - last.Date).TotalSeconds >= 20)
{
yield return item;
}
previous = last;
last = item;
}
if (previous != null && last != null && (last.Date - previous.Date).TotalSeconds <= 20)
yield return last;
}
Of course it would be possible to make it more reusable, by making the method generic and passing a predicate as a parameter, but since it's a very specific requirement, I'm not sure it would be very useful...

Categories

Resources