I am trying to figure out the best way to search a list of objects for when a certain condition is met within a 24-hour window. I would prefer the 24-hour window be flexible enough that it isn't going by days but just 24-hours. My objects would look like something below.
public class Event {
public DateTime timestamp;
public string reason;
public long amount;
}
So it would search through this list for when the reason equalled something specific and it there was 10 of these within a 24-hour window it would return a list of those 10 along with any extra ones incase there was more.
To search for ALL 24-hour periods (not just 24 hours prior to now or an arbitrary range), you could do:
string reason = "???";
var query =
events.Where(ev => ev.Reason = reason)
.Select(ev => events.Where(ev2 => ev.Reason = reason &&
(ev.timestamp >= ev2.timestamp) &&
(ev.timestamp - ev2.timestamp).TotalHours <= 24))
.Where(g => g.Count() >= 10);
Transation: for each event, get all events within the 24 hours prior to it, and return all groups where that are at least 10 such events.
You can add hours to a DateTime instance and then compare your time
var startOfWindow = DateTime.Now \\ or however you get your window start
var endOfWindow = startOfWindow.AddHours(24);
var interestingEvents = events
.Where(e => e.reason == "reason")
.Where(e=> startOfWindow <= e.timestamp && e.timestamp <= endOfWindow)
.ToList();
Use LINQ
var search = events.Where(ev => ev.reason == "reason" && ev.timestamp >= DateTime.Now.AddHours(-24));
Have a method to filter and then return results like:
public IEnumerable<Event> GetEvents(List<Event> list, string yourReason)
{
var query = list.Where(e => e.reason == "your reason" &&
e.timestamp >= DateTime.Now.AddHours(-24));
if (query.Count() >= 10)
return query;
else
return null;
}
Maybe this will do the trick
public IEnumerable<Event> EventsInTimeWindow(
IEnumerable<Event> events,
DateTime from,
DateTime to,
string reason,
int maxNoOfResults)
{
return events
.Where(evt =>
from <= evt.timestamp &&
evt.timestamp <= to &&
evt.reason == reason)
.Take(maxNoOfResults);
}
Use:
DateTime now = DateTime.Now;
var events = EventsInTimeWindow(someCollection, now.AddHours(-24), now, 15);
Related
I am working on a staff rota app and want to sum only the hours below a threshold of 2250 minutes (37.5 hours). I am struggling to isolate these hours however and the reason being is as follows. Firstly I'm new to LINQ.
Secondly, there are two different pay types that can be entered into the app, so I have to sum both pay types using .Sum() which is fine. The problem I'm having is isolating only the summed hours below 37.5 hours
I am grouping the results and running something like
g.Sum(x => x.Start >= start && x.End <= End ? x.Type1 : 0) +
g.Sum(x => x.Start >= start && x.End <= End ? x.Type2 : 0) <= 2250 ? .....
// then count the hours below here.
Now I get that this example will return zero if the count exceeds 2250, but how do I create a subset of the values below 2250 only?
Assuming that Type1 is minutes worked for pay type 1 and Type2 is minutes worked for pay type 2, you could go about it as follows:
Filter the relevant items based on your Start and End conditions
For each item, calculate the total minutes worked (Type1 + Type2)
Filter the total minutes by comparing them to the threshold
Summing total minutes items that are below the threshold
In Linq,
filtering can be done using .Where()
creating a new object for each item in the collection can be done using .Select()
Implementation:
int threshold = 2250;
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Select(x => x.Type1 + x.Type2)
.Where(minutesWorked => minutesWorked <= threshold)
.Sum();
Another possible approach is to do all the filtering first, and then sum the total minutes worked:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Where(x => x.Type1 + x.Type2 <= threshold)
.Sum(x => x.Type1 + x.Type2);
These implementations will only take into account the work time of the employees that have worked less than or equal to the threshold.
If you rather need to include work time for all employees, but limit the maximum work time that is included in the calculation for each employee to the threshold (i.e. for each employee, use sum = x.Type1 + x.Type2 if sum is less than or equal to the threshold; else, use the threshold), you may utilize Math.Min() to get the lowest value of the total minutes worked (x.Type1 + x.Type2) and the threshold.
The implementation can now be simplified:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Sum(x => Math.Min(x.Type1 + x.Type2, threshold));
If Type1 and/or Type2 are nullable (e.g. int?), you need to ensure that Math.Min() actually has int values to work with. You will then need to provide a fallback for each nullable value.
This can be achieved by replacing x.Type* with (x.Type* ?? 0), which reads:
Take the value of x.Type* if x.Type* is not null; else, take 0.
If both Type* properties are nullable, the implementation hence becomes:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Sum(x => Math.Min((x.Type1 ?? 0) + (x.Type2 ?? 0), threshold));
If you cannot use Math.Min(), you could perhaps rather use a ternary operator to select the desired work time portion for each employee. I would then first calculate the total minutes worked for each employee, and then decide if the total minute amount or the threshold value should be used:
int filteredTotalMinutes = g
.Where(x => x.Start >= start && x.End <= end)
.Select(x => (x.Type1 ?? 0) + (x.Type2 ?? 0))
.Sum(minutesWorked => minutesWorked < threshold
? minutesWorked
: threshold);
This question already has answers here:
How to get the next working day, excluding weekends and holidays
(5 answers)
Closed 3 years ago.
I set up some methods to get holidays and used them to calculate next working day. This works fine but it only adds 1 day and gets the next non holiday weekday. I want to get the number of days by input and increment that date according to input and get the nex non holiday weekday. I tried some things but couldnt get exactly what i want.
Edit: I am using a method to get the next working day, excluding weekends and holidays. But my equation does that by incrementing the date only by 1 day. I want that number of days to be my input from user, and add that number to the date, exclude weekends and holidays and get the next working day. (My issue is on the adding that different number from my textbox)
public static DateTime GetWorkingDay(DateTime date, IList<Holiday> holidays, IList<DayOfWeek> weekendDays)
{
date = date.Date.AddDays(1);
var holidayDates = holidays.Select(x => x.GetDate(date.Year))
.Union(holidays.Select(x => x.GetDate(date.Year + 1)))
.Where(x => x != null)
.Select(x => x.Value)
.OrderBy(x => x).ToArray();
while (true)
{
if (weekendDays.Contains(date.DayOfWeek) || holidayDates.Contains(date))
date = date.AddDays(1);
else
return date;
}
}
You could change your while loop to (assuming numDays is your input):
while (numDays > 0)
{
date = date.AddDays(1);
if (!weekendDays.Contains(date.DayOfWeek) && !holidayDates.Contains(date))
numDays--; // Only decrement numDays if it's a working day.
}
Then return the date after the loop. You might want to assert that numDays isn't negative.
Here's an approach that might help. Or it might seem like total overkill. You decide.
Instead of trying to add one day at a time, figure out if it's a holiday, weekday, etc., just create a list of dates that you can query. That will make the logic much easier to understand, especially if you find yourself having to write more and more queries like this.
Ideally this is something you'd want to create once and keep around. It wouldn't make sense to keep creating this over and over.
Here's a stab at it:
public class Calendar
{
private readonly List<CalendarDate> _dates = new List<CalendarDate>();
public Calendar(
DateTime startDate,
int length,
ICollection<DateTime> holidays,
ICollection<DayOfWeek> weekDays)
{
if(length < 1)
throw new ArgumentOutOfRangeException(nameof(length), "The minimum length is one day.");
for (var x = 0; x < length; x++)
{
var addingDate = new CalendarDate
{
RepresentedDate = startDate.Date.AddDays(x),
};
addingDate.IsHoliday = holidays.Contains(addingDate.RepresentedDate);
addingDate.IsWeekday = weekDays.Contains(addingDate.RepresentedDate.DayOfWeek);
_dates.Add(addingDate);
}
FirstDay = startDate.Date;
LastDay = FirstDay.AddDays(length - 1);
}
public DateTime FirstDay { get; }
public DateTime LastDay { get; }
private class CalendarDate
{
internal DateTime RepresentedDate { get; set; }
internal bool IsHoliday { get; set; }
internal bool IsWeekday { get; set; }
internal bool IsWorkingDay => !(IsHoliday || IsWeekday);
}
public DateTime GetNextWorkingDay(DateTime date)
{
date = date.Date;
ValidateDateIsInRange(date);
var result = _dates.FirstOrDefault(d =>
d.RepresentedDate >= date && d.IsWorkingDay);
if (result != null) return result.RepresentedDate;
throw new InvalidOperationException("The result is outside the range of the calendar.");
}
private void ValidateDateIsInRange(DateTime date)
{
if(date < FirstDay || date > LastDay)
throw new InvalidOperationException("The specified date is outside the range of the calendar.");
}
}
It admittedly looks like more work and more code. But the result is that once the data is in place, the calculation you're trying to do becomes very easy. So do all sorts of other calculations and queries, because now such functions can be written to reflect the questions that you're asking, like "What is the first date that matches this criteria?"
I have System.DateTime object which I need to confirm is on the hour.
So what would be the most efficient way of checking this?
The current solution that I have thought of involves converting the object ToString() and obtaining the minutes/seconds section to see if they equal zero. If there are easier ways to do this I would appreciate suggestions!
DateTime has minute and seconds properties, you could directly check that, no need to convert. You could do the same for milliseconds, if that is relevant to you.
You could go this way with highest accuracy.
if(date.Ticks % TimeSpan.TicksPerHour == 0)
My personal solution would be to create a new DateTime which is on the hour and compare it with the one I wish to check, something like:
public bool IsOnHour(DateTime dateTime)
{
var onHour = dateTime.Date + TimeSpan.FromHours(dateTime.Hour);
return onHour == dateTime;
}
Then I don't need to check the Minutes, Seconds, Milliseconds etc.
Try following .
var dt = new DateTime();
if (dt.Minute == 0 && dt.Second == 0 && dt.Millisecond == 0)
Here's a programatic way:
DateTime min = new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0);
DateTime max = min.AddMinutes(1);
if (min <= value && value < max)
This intentionally looses precision so that 12:59:00.000 is the same as 12:59:27.198.
You can use this: if(DateTime.Now.Minute == 0 && DateTime.Now.Second == 0 && DateTime.Now.Millisecond == 0)
Edit: comments
I'm trying to build a Linq query that filter between 2 times of day.
First I need to filter between dates (ie.: 12/26/2013 to 01/26/2014) and after this search, the results may be filtered between times (ie.: records between 19:00 and 7:00).
I've tried with this query but it does not works:
orders = CurrentOrders.Where(o => o.ImportedOn >= dateFrom && o.ImportedOn <= dateTo);
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
tFrom and tTo both are TimeSpan.
Any help?
Regards
Edit: Hi I'm editing my own question just to clarify the problem and attach a solution that I've found (I'm not sure if it's the most efficient) :
When I say "does not work" I mean "no records are returned that matched the criteria".
The previous query works only for time between the same day (ie: from 15:00 to 20:00). The problem is when the time span to the next day, for example form 19:00pm to 7:00am.
The solution that I propose is add a simple if to check if tFrom is less than tTo and otherwise add 1 to date in the search:
if (tFrom < tTo)
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
}
else
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay <= tFrom && o.ImportedOn.AddDays(1).TimeOfDay <= tTo);
}
I don't think your queries will give you results when the time range is 9 PM to 4 AM for example. Let's say that ImportedOn is 7 PM and you are checking that 7 PM is less than 9 PM, which is OK, and also checking that 7 PM is less than 4 AM, which is false. I don't see any difference if you add a day because you are only considering the time. Adding a date doesn't change the time.
My proposal is to create two time intervals when the time from is greater than time to (9 PM to 4 AM for example).
I created an extension method for DateTime so we can check if the date belongs to a time range.
public static bool IsInTimeRange(this DateTime obj, DateTime timeRangeFrom, DateTime timeRangeTo)
{
TimeSpan time = obj.TimeOfDay, t1From = timeRangeFrom.TimeOfDay, t1To = timeRangeTo.TimeOfDay;
// if the time from is smaller than the time to, just filter by range
if (t1From <= t1To)
{
return time >= t1From && time <= t1To;
}
// time from is greater than time to so two time intervals have to be created: one {timeFrom-12AM) and another one {12AM to timeTo}
TimeSpan t2From = TimeSpan.MinValue, t2To = t1To;
t1To = TimeSpan.MaxValue;
return (time >= t1From && time <= t1To) || (time >= t2From && time <= t2To);
}
Edited: Note that it is not necessary to compare time with t2From and t1To because the comparison is always gonna be true but it makes the code easier to read because it explicitly checks that the date belongs to one of the two intervals.
I also wrote these unit tests:
[TestMethod]
public void TimeRangeFilter_timeFrom_is_smaller_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 AM to 4 PM
DateTime timeFrom = DateTime.Today.AddHours(10), timeTo = DateTime.Today.AddHours(16);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 23));
}
[TestMethod]
public void TimeRangeFilter_timeFrom_is_greater_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 PM to 4 AM
DateTime timeFrom = DateTime.Today.AddHours(22), timeTo = DateTime.Today.AddHours(4);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 23));
}
ImportedOn.TimeOfDay
returns just a Time. For example:
DateTime t = DateTime.Now;
System.Diagnostics.Debug.WriteLine(t.ToString());
var t2 = t.TimeOfDay;
System.Diagnostics.Debug.WriteLine(t2.ToString());
returns:
02.06.2014 11:48:33
11:48:33.6671525
So you can just compare the Dates, there is no need of TimeOfDay.
Linq to SQL: how to query a time range in a DateTime field
If you are tring to select on a table the records that have a DateTime field (in this example called DateFiled) between two DateTime value, if you specify:
DateFiled >= dateValue && DateFiled <= dateValue
you select all the record between this value, but if you need to select only the records that between these two dates correspond to a specific time range, you’ll need to add a column that computes the hour.
If you do not want to add it to the database, you will need to add it to your model and set it with the same value as the DateField field. For example:
public async Task<List<MyModel>> GetFilteredResultByDateAndTime
(DateTime startDate, DateTime endDate)
{
var result = from mytable in _context.MyTable
where
mytable.DateField >= startDate.Date
&& mytable.DateField <= endDate.Date
select new MyModel
{
DateField = mytable.DateField.Date,
DateFieldTime = mytable.DateField,
// Other fields of the model
};
// Now you can filter the result by the time
var filteredResult = from r in result
where
r.DateFieldTime.TimeOfDay >= startDate.TimeOfDay
&& r.DateFieldTime.TimeOfDay <= endDate.TimeOfDay
select r;
return await filteredResult.ToListAsync();
}
Edit:
Steps:
Start at target day.
Then move backwards until no events are carried over from another day.
From there, start counting hours, and keep track of carried over hours.
Day cannot last more than ActualDayLength()
Then, once you know that, work your way back to target and then calculate actual occupied hours.
I have tasks that are put on a calendar:
Now let me give this some context:
Each day 'lasts' 7.5 hours here. But I work with a variable called DayHours (which right now is 7.5). (DayHours is also used in Locked Time which Ill describe below).
The goal of this calendar is to schedule 7.5 hour work days for employees.
What I need, is an algorithm that can correctly tell me how many hours are actually occupied in a day.
This seems simple, but is actually quite recursive.
First, a couple of notes. You will notice Case manager, at 14 hours, could be done in 2 days of 7.5 hours with 1 hour left over. It is stretched to 3 days because 1. Schedule, is 5 hours long, and 2. cannot start until the predecessor tasks of the day are complete.
There is also the concept of Locked Time.
In purple is Locked Time. This is a 10 hour block of locked time.
This means, on the 12th, I can only do (7.5 - 7.5) hours of work, and Monday, only (7.5 - 2.5) aswell.
I already have a function to calculate an actual day's available hours to account for this:
public decimal GetActualDayLength(DateTime day, Schedule s)
{
var e = Schedules.GetAllWithElement();
var t = Timeless(day);
var locked = from p in e
where p.EmployeID == s.EmployeID &&
((p.DateTo.Value.Date) >= t &&
Timeless(p.DateFrom.Value) <= t) &&
p.IsLocked
select p;
decimal hrs = 0.0M;
foreach (var c in locked)
{
if (c.Hours.Value <= DaysManager.GetDayHours())
hrs += c.Hours.Value;
else if (Timeless(c.DateTo.Value) != t)
hrs += DaysManager.GetDayHours();
else
{
if (c.Hours.Value % DaysManager.GetDayHours() > 0)
hrs += c.Hours.Value % DaysManager.GetDayHours();
else
hrs += DaysManager.GetDayHours();
}
}
return DaysManager.GetDayHours() - hrs;
}
There is also the concept of carry hours.
Here is an example:
Now let us take Thursday the 18th (The 18th has 1. Case):
To find the number of hours this day has for that employee, we need to first look at the tasks that start, end, or fall within that day.
I don't know how many hours I can do on the 18th because the task ending that day might have had carry hours.
So I go look at Perform unit test's start day. I cant figure that out either because NWDM finishes that day and it might have carry hours.
So now I go evaluate NWDM. Ahh, this one has nothing ending that day, so I know Schedule will take 5 / 7.5 hours available.
So I keep going, adding 7.5 hours each day that I pass.
Then I get to NWDM's last day.
Up until then, I worked 5 + 7.5 + 7.5 + 7.5 hours on it,
So I put in 27.5 hours, so I'll put in (30 - 27.5 = 2.5h) on the 22nd to finish it. So I have 5 hours left to work on Perform Unit Tests.
This means that I will need 1.5h to finish it. Now Case is 1 hour long.
Had case been 7.5 - 1.5 or more, we say the day is full and return DayHours.
Therefore, we are done. The return value is 1.5 + 1 = 2.5.
The function should look a bit like this one:
public decimal GetHours(IEnumerable<Schedule> s, DateTime today)
{
DateTime t = Timeless(today);
decimal hrs = 0;
foreach (Schedule c in s)
{
if (c.Hours.Value <= DaysManager.GetDayHours())
hrs += c.Hours.Value;
else if (Timeless(c.DateTo.Value) != t)
hrs += DaysManager.GetDayHours();
else
{
if (c.Hours.Value % DaysManager.GetDayHours() > 0)
hrs += c.Hours.Value % DaysManager.GetDayHours();
else
hrs += DaysManager.GetDayHours();
}
}
return hrs;
}
To get the events that start, end, or fall within a given day, I use:
public IEnumerable<Schedule> GetAllToday(DateTime date, int employeeID, Schedule current)
{
DateTime t = Timeless(date);
int sid = current == null ? -1 : current.ScheduleID;
var e = Schedules.GetAllWithElement();
return from p in e
where (((Timeless(p.DateTo.Value) >= t &&
Timeless(p.DateFrom.Value) <= t &&
p.EmployeID == employeeID) &&
(p.IsLocked || (Timeless(p.DateFrom.Value) < t &&
(sid == -1 ? true : Timeless(p.DateFrom.Value) < current.DateFrom.Value)) ||
bumpedList.Any(d => d.ScheduleID == p.ScheduleID)) &&
p.ScheduleID != sid) ||
((Timeless(p.DateTo.Value) >= t &&
(Timeless(p.DateFrom.Value) == t || (Timeless(p.DateFrom.Value) < t &&
(sid == -1 ? true : Timeless(p.DateFrom.Value) > current.DateFrom.Value))) &&
p.EmployeID == employeeID) &&
!p.IsLocked &&
!bumpedList.Any(d => d.ScheduleID == p.ScheduleID) &&
p.ScheduleID != sid)) &&
p.ScheduleID != sid
select p;
}
The Schedule has the following relevant fields:
DateFrom
DateTo
Hours
EmployeeID
The Schedule looks something like:
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Schedule")]
public partial class Schedule : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _ScheduleID;
private System.Nullable<System.DateTime> _DateFrom;
private System.Nullable<decimal> _Hours;
private System.Nullable<int> _EmployeID;
private System.Nullable<int> _RecurringID;
private System.Nullable<int> _Priority;
private System.Nullable<System.DateTime> _DateTo;
private bool _IsLocked;
private System.Nullable<int> _BumpPriority;
private EntitySet<Case> _Cases;
private EntitySet<Project> _Projects;
private EntitySet<Task> _Tasks;
private EntitySet<Task> _Tasks1;
private EntityRef<Employee> _Employee;
private EntityRef<Recurring> _Recurring;
#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnScheduleIDChanging(int value);
partial void OnScheduleIDChanged();
partial void OnDateFromChanging(System.Nullable<System.DateTime> value);
partial void OnDateFromChanged();
partial void OnHoursChanging(System.Nullable<decimal> value);
partial void OnHoursChanged();
partial void OnEmployeIDChanging(System.Nullable<int> value);
partial void OnEmployeIDChanged();
partial void OnRecurringIDChanging(System.Nullable<int> value);
partial void OnRecurringIDChanged();
partial void OnPriorityChanging(System.Nullable<int> value);
partial void OnPriorityChanged();
partial void OnDateToChanging(System.Nullable<System.DateTime> value);
partial void OnDateToChanged();
partial void OnIsLockedChanging(bool value);
partial void OnIsLockedChanged();
partial void OnBumpPriorityChanging(System.Nullable<int> value);
partial void OnBumpPriorityChanged();
#endregion
public Schedule()
{
this._Cases = new EntitySet<Case>(new Action<Case>(this.attach_Cases), new Action<Case>(this.detach_Cases));
this._Projects = new EntitySet<Project>(new Action<Project>(this.attach_Projects), new Action<Project>(this.detach_Projects));
this._Tasks = new EntitySet<Task>(new Action<Task>(this.attach_Tasks), new Action<Task>(this.detach_Tasks));
this._Tasks1 = new EntitySet<Task>(new Action<Task>(this.attach_Tasks1), new Action<Task>(this.detach_Tasks1));
this._Employee = default(EntityRef<Employee>);
this._Recurring = default(EntityRef<Recurring>);
OnCreated();
}
}
Could anyone help me with developing an algorithm that can do this?
Even though you question is very complex and not very clearly explained, I'll try to answer it. Or more precisely hint you how you should decompose and solve it (or how I would solved it).
What I need, is an algorithm that can correctly tell me how many hours are actually occupied in a day.
At first I do not see the real problem in case you have DateTo value available for Schedule. Unless it equals to DateFrom + Hours. In such case it does not reflect the real DateTo but somewhat irrelevant value instead.
I will presume any Schedule is defined by starting time DateFrom and duration Hours. DateTo is calculated value and efficiently computing is the real core of the problem.
So I think this function getting available hours in any time range is pretty straightforward. Speaking in pseudo-code:
TimeSpan GetAvailableTime(DateRange range)
var tasks = FindIntersectingTasks(range)
' now the algorithm which finds available hours on given collection
' of tasks
' firstly - we need to determine relevant ranges which intersect
' with given range
var occupiedRanges = New List<DateRange>(tasks.Count)
for each task in tasks
var intersection = range.Intersect(
new DateRange(task.DateFrom, task.DateTo)
)
if Not intersection.IsEmpty
occupiedRanges.Add(intersection)
end
end
' secondly - sort ranges by start so we can easily merge them
ranges.Sort(range => range.DateFrom)
var mergedOccupiedRanges = new List(DateRange)
' thirdly - merge ranges so that we have collection with
' non-overlaping ranges (and also sorted)
for each occupiedRange in occupiedRanges
' range may merge only it there is non-empty intersection
if occupiedRange.CanMerge(mergedOccupiedRanges.Last)
var mergedRange = range.Merge(mergedOccupiedRanges.Last)
mergedOccupiedRanges.RemoveLast()
mergedOccupiedRanges.Add(mergedRange)
end
end
' fourthly - it is simple now to determine available/occupied hours
var timeAvailable = range.Duration
for each mergedRange in mergedOccupiedRanges
timeAvailable -= mergedRange.Duration
end
return timeAvailable
end
IEnumerable<Schedule> FindIntersectingTasks(DateRange range)
return From schedule In allEvents
Where schedule.DateFrom <= range.To
And schedule.DateTo >= range.From
end
You may need some adjustments as the DateTime expects normal 24-hour days.
Like such:
create a list of days with free time.
for each item in the list, add the maximum amount of time available to your task.
If the time needed for your project reaches 0, stop adding blocks.
This does not answer the exact question, but I would suggest simplifying your logic by extending your classes (objects) with some helper methods, e.g. methods/properties that return lists of days occupied.
If you cannot access these classes (i.e. they are not from your code base) - then create new classes and map to those.
Also - the .NET DateTime class has some very useful properties and enums like "DayOfWeek" and "TimeOfDay" that might be useful for you.