I am working on an algorithm to calculate a continuous overlap of multiple date ranges. It also needs to have a set number of times that it overlaps. For the example image below, I need 3 dates to overlap continuously. The valid overlapping dates would be Aug 20 - Aug 23, as Aug 24 only has 2 overlapping.
I have attempted many approaches including looping through all dates and comparing each one indivually with the next. That code looks like this.
Here is a .net fiddle for better visualization: https://dotnetfiddle.net/x3LfHR#.
private bool Compare(CompareDate a, CompareDate b)
{
DateTime? tStartA = a.ActiveDate;
DateTime? tEndA = a.ExpireDate;
DateTime? tStartB = b.ActiveDate;
DateTime? tEndB= b.ExpireDate;
bool overlap = (tStartA <= tEndB || tEndB == null) && (tStartB <= tEndA || tEndA == null);
DateTime? overlapStart = null;
DateTime? overlapEnd = null;
if (overlap)
{
//Find maximum start date.
overlapStart = (tStartA >= tStartB) ? tStartA : tStartB;
//Find Min End date between the two
overlapEnd = (tEndA <= tEndB) ? tEndA : tEndB;
if (overlapStart > this.overlapStart || this.overlapStart == null)
{
this.overlapStart = overlapStart;
}
if (overlapEnd < this.overlapEnd || this.overlapEnd == null)
{
this.overlapEnd = overlapEnd;
}
However, this approach makes it tricky to figure out continuous overlap dates. I have attempted to use the .Net Time period library at https://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET, but its not relevant in my case. Any help is appreciated.
OK - LINQ to the rescue!
Note: In order to make the comparison work, you must remove the time component and strictly use the only the date (e.g. DateTime.Date). Based on your requirements, that's exactly how you need to do it, so it shouldn't be a problem.
public List<DateTime> CompareDates(List<DateTime[]> compareRanges, int overlapLevel = 1)
{
var grouped = compareRanges.SelectMany(r => r).GroupBy(d => d);
var thresholdMatch = grouped.Where(g => g.Count() >= overlapLevel)
.Select(g => g.Key)
.OrderBy(d => d)
.ToList();
return thresholdMatch;
}
You can test the logic in a sample console app, using the skeleton code below as an example:
static void Main()
{
var setA = new[]
{
new DateTime(2017, 8, 20),
new DateTime(2017, 8, 21),
new DateTime(2017, 8, 22),
new DateTime(2017, 8, 23),
new DateTime(2017, 8, 24),
};
var setB = new[]
{
new DateTime(2017, 8, 20),
new DateTime(2017, 8, 21),
new DateTime(2017, 8, 22),
};
var setC = new[]
{
new DateTime(2017, 8, 22),
new DateTime(2017, 8, 23),
new DateTime(2017, 8, 24),
new DateTime(2017, 8, 25),
new DateTime(2017, 8, 26),
};
var setD = new[]
{
new DateTime(2017, 8, 20),
new DateTime(2017, 8, 21),
new DateTime(2017, 8, 22),
new DateTime(2017, 8, 23),
};
var compareList = new List<DateTime[]>
{
setA, setB, setC, setD
};
// setting the threshold to 2 will cause 8/24 to be added to the result...
// setting this to 1 (default) will return all intersections
// for now, set it to 3, per the question!
var result = CompareDates(compareList, 3);
foreach (var intersectDate in result)
{
Console.WriteLine(intersectDate);
}
}
Hope this helps, I certainly had fun with it!
P.S. I forked your fiddle: https://dotnetfiddle.net/GUzhjh.
This contains the modified version of your original program, so you should be able to play around with it a bit.
Here is a start at an algorithm:
Sort all start and end datetimes, assigning a +1 to every start and -1 to every end.
proceed from interval-start to interval-end, aggregating the period assignments above looking for a +3 value. Note this datetime.
proceed forward until your aggregate value drops below +3.
Voila! (I think.) Remember this one and continue processing.
When a second occurs, then save the longest and discard the other.
Continue until end-interval found; and report result.
Related
I'm trying to make a function in C# that returns the week difference between two dates. Its goal is to provide the same result of:
select datediff(ww,'2018-04-13','2018-04-16') as diff
In the example above there is only 3 days between these dates, but they are in different weeks, so the result should be 1.
I've tried to use .TotalDays but it's not working properly. I also tried .GetWeekOfYear but it won't return correctly when the year of the dates are different. I've seem many questions here on StackOverflow and on other forums and so far none of them match my case. This is the function I'm trying to far:
public static int GetWeekDiff(DateTime dtStart, DateTime dtEnd) {
// Doesn't work
var val = ((dtEnd - dtStart).TotalDays / 7);
val = Math.Ceiling(val);
return Convert.ToInt32(val);
// Doesn't work well between years
DateTimeFormatInfo dinfo = DateTimeFormatInfo.CurrentInfo;
var x = dinfo.Calendar.GetWeekOfYear(dtStart, CalendarWeekRule.FirstFullWeek, DayOfWeek.Monday);
var y = dinfo.Calendar.GetWeekOfYear(dtEnd, CalendarWeekRule.FirstFullWeek, DayOfWeek.Monday);
return y - x;
}
In the first part of my function, I tried what is described in this post. It didn't work
Can you help me?
Thanks in advance.
First figure how many days there are between the two dates. Divide the number of days by 7 to get full weeks.
Now figure out if there's an extra week to be counted by finding taking the number of days modulus 7 to get any remaining days. If the first date plus remaining days falls in a different week, add an extra week on to the count.
void Main()
{
var first = new DateTime(2018, 04, 13);
var second = new DateTime(2018, 04, 16);
Console.WriteLine(weekDiff(first, second));
}
public int weekDiff(DateTime d1, DateTime d2, DayOfWeek startOfWeek = DayOfWeek.Monday)
{
var diff = d2.Subtract(d1);
var weeks = (int)diff.Days / 7;
// need to check if there's an extra week to count
var remainingDays = diff.Days % 7;
var cal = CultureInfo.InvariantCulture.Calendar;
var d1WeekNo = cal.GetWeekOfYear(d1, CalendarWeekRule.FirstFullWeek, startOfWeek);
var d1PlusRemainingWeekNo = cal.GetWeekOfYear(d1.AddDays(remainingDays), CalendarWeekRule.FirstFullWeek, startOfWeek);
if (d1WeekNo != d1PlusRemainingWeekNo)
weeks++;
return weeks;
}
static void Main(string[] args)
{
DateTime date1 = new DateTime(2018, 04, 18);
DateTime date2 = new DateTime(2018, 04, 19);
System.Console.WriteLine((GetDiff(new DateTime(2018, 04, 18), new DateTime(2018, 04, 18)))); // 0
System.Console.WriteLine((GetDiff(new DateTime(2018, 04, 22), new DateTime(2018, 04, 23)))); // 1
System.Console.WriteLine((GetDiff(new DateTime(2018, 04, 16), new DateTime(2018, 04, 22)))); // 0
System.Console.WriteLine((GetDiff(new DateTime(2018, 04, 18), new DateTime(2018, 05, 03)))); // 2
}
private static int GetDiff(DateTime date1, DateTime date2)
{
date1 = SetDayToMonday(date1);
date2 = SetDayToMonday(date2);
return (int)((date2 - date1).TotalDays / 7);
}
private static DateTime SetDayToMonday(DateTime date)
{
var weekDay = date.DayOfWeek;
if (weekDay == DayOfWeek.Sunday)
return date.AddDays(-6);
else
return date.AddDays(-((int)weekDay-1));
}
First, set the day to the monday of the current week. Then count all full weeks(= /7 days as int). Easy as it is, it works probably across weeks and years.
See if this works. There could be more use cases that this doesn't cover, and the solution depends on how you define a week boundary (this assumes Sunday-Monday based on a comment above).
// Output:
// Weeks between 12/28/2017 and 1/10/2018: 2
// Weeks between 4/13/2018 and 4/16/2018: 1
// Weeks between 4/21/2018 and 4/22/2018: 0
// Weeks between 4/22/2018 and 4/23/2018: 1
void Main()
{
var datePairs = new List<KeyValuePair<DateTime, DateTime>>();
datePairs.Add(new KeyValuePair<DateTime, DateTime>(new DateTime(2017, 12, 28), new DateTime(2018, 1, 10)));
datePairs.Add(new KeyValuePair<DateTime, DateTime>(new DateTime(2018, 4, 13), new DateTime(2018, 4, 16)));
datePairs.Add(new KeyValuePair<DateTime, DateTime>(new DateTime(2018, 4, 21), new DateTime(2018, 4, 22)));
datePairs.Add(new KeyValuePair<DateTime, DateTime>(new DateTime(2018, 4, 22), new DateTime(2018, 4, 23)));
foreach (var datePair in datePairs)
{
var string1 = datePair.Key.ToShortDateString();
var string2 = datePair.Value.ToShortDateString();
Console.WriteLine($"Weeks between {string1} and {string2}: {GetWeekDiff(datePair.Key, datePair.Value)}");
}
}
public static int GetWeekDiff(DateTime dtStart, DateTime dtEnd)
{
var totalDays = (dtEnd - dtStart).TotalDays;
var weeks = (int)totalDays / 7;
var hasRemainder = totalDays % 7 > 0;
if (hasRemainder)
{
if (!(dtStart.DayOfWeek.Equals(DayOfWeek.Saturday) && dtEnd.DayOfWeek.Equals(DayOfWeek.Sunday)))
{
weeks++;
}
}
return weeks;
}
Maybe it can help
public static int GetIso8601WeekOfYear(DateTime time)
{
// Seriously cheat. If its Monday, Tuesday or Wednesday, then it'll
// be the same week# as whatever Thursday, Friday or Saturday are,
// and we always get those right
DayOfWeek day = CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(time);
if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)
{
time = time.AddDays(3);
}
// Return the week of our adjusted day
return CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(time, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
Get the correct week number of a given date
Can't comment yet and already used a flag on this post on something I believed to be similar. Here is another post I found that appears to align with the solution you are trying to create:
Get the number of calendar weeks between 2 dates in C#
This is my implementation to solve a similar problem, I haven't tested in thoroughly but it seems to work.
var dt1 = DateTime.Today.AddDays(-30);
var dt2 = DateTime.Today;
var noOfDays =(int) (dt2 - dt1).TotalDays;
int reminder;
var weeks = Math.DivRem(noOfDays, 7, out reminder);
weeks = reminder > 0 ? weeks + 1 : weeks;
It returns 1 week for 6 days or less gap, which is exactly what I needed.
i have a list of "event" objects.
In every event i have "EventStartTime" and "EventEndTime" declared as DateTime objects.
I want to be able to search "events" by time , for example 10:00,
the "event" you see below shows that the festival starts at 22:00 on Feb 17th,
and ends at 15:00 the following day. i have a couple more like these.
new EventsManager.Event() //3
{
EventType = EventsManager.EventType.Festival,
EventName = "Twistival",
EventPlace = placeList[4],
EventStartTime =new DateTime(2017,02,17,22,0,0),
EventEndTime = new DateTime(2017,02,18,15,0,0),
EventNumberOfParticipants = 8000
},
So when i search for event that occur, or still occurring at at 10:00
i should get this event.
any suggestions?
Assuming that you have a specific time of day that you want to determine if the event covers regardless of the date it covers it on then there are 4 cases you need to consider. First if the dates are more than 1 day apart they cover all times of day. If the start is before the time of day and the end is after the time of day it will cover the time. The last two cases require that the end date be on the next day from the start date, then either the start date is before the time of day, or the end date is after the time of day. Note that this also assumes that the start date is before the end date.
var events = new List<Tuple<DateTime, DateTime>>
{
// start and end after time of day but on different days
Tuple.Create(
new DateTime(2017, 02, 17, 22, 0, 0),
new DateTime(2017, 02, 18, 15, 0, 0)),
// start and end before time of day but on different days
Tuple.Create(
new DateTime(2017, 02, 17, 9, 0, 0),
new DateTime(2017, 02, 18, 7, 0, 0)),
// start before and end after same day
Tuple.Create(
new DateTime(2017, 02, 17, 9, 0, 0),
new DateTime(2017, 02, 17, 11, 0, 0)),
// covers more than 1 day
Tuple.Create(
new DateTime(2017, 02, 17, 22, 0, 0),
new DateTime(2017, 02, 18, 22, 0, 1)),
// start after and end before on different days
Tuple.Create(
new DateTime(2017, 02, 17, 22, 0, 0),
new DateTime(2017, 02, 18, 10, 0, 0)),
// start and end before on same day
Tuple.Create(
new DateTime(2017, 02, 17, 7, 0, 0),
new DateTime(2017, 02, 17, 8, 0, 0)),
// start and end after on same day
Tuple.Create(
new DateTime(2017, 02, 17, 11, 0, 0),
new DateTime(2017, 02, 17, 12, 0, 0)),
};
var timeOfDay = new TimeSpan(0, 10, 0 ,0);
foreach (var x in events)
{
if (x.Item2 - x.Item1 > TimeSpan.FromDays(1)
|| (x.Item1.TimeOfDay < timeOfDay && x.Item2.TimeOfDay > timeOfDay)
|| (x.Item1.Date < x.Item2.Date
&& (x.Item1.TimeOfDay < timeOfDay || x.Item2.TimeOfDay > timeOfDay)))
{
Console.WriteLine(x);
}
}
Will output
(2/17/2017 10:00:00 PM, 2/18/2017 3:00:00 PM)
(2/17/2017 9:00:00 AM, 2/18/2017 7:00:00 AM)
(2/17/2017 9:00:00 AM, 2/17/2017 11:00:00 AM)
(2/17/2017 10:00:00 PM, 2/18/2017 10:00:01 PM)
Let's say you have a
List<Event> Events;
of your Events. You can create a simple LINQ query to get all events running at a special time with a simple method like
private IEnumerable<Event> GetRunningEvents(DateTime time)
{
return Events.Where(E => E.EventStartTime <= time && E.EventEndTime >= time);
}
Dont forget to add
using System.Linq;
to your file.
EDIT: Without LINQ a possible approach is
private List<Event> GetRunningEvents(DateTime time)
{
List<Event> RunningEvents = new List<Event>();
foreach(Event E in Events)
{
if (E.EventStartTime <= time && E.EventEndTime >= time)
{
RunningEvents.Add(E);
}
}
return RunningEvents;
}
Try Linq Where:
var list = new List<Event>();
var searchTime = DateTime.Now;
var result = list.Where(e => e.EventStartTime <= searchTime && searchTime <= e.EventEndTime).ToList();
I'm trying to calculate a due date for a service level agreement, and at the same time, I also need to back calculate the service level agreement in the other direction.
I've been struggling with calculations for "working time" (i.e. the time that work is possible during a set of days), and decided to use a third party library called TimePeriodLibrary.NET for the task. I need to be able to do two things:
Given a start DateTime and a TimeSpan, you should receive a DateTime of when a service level agreement date is due (date due).
Given a start DateTime and an end DateTime, you should receive a TimeSpan of how long that service level agreement should take.
All source code (test project is on GitHub). I have a ServiceLevelManager class that does all the work. It take a list of WorkDays and HolidayPeriods, in order to work out which hours are available to be worked. The CalendarPeriodCollector class is giving unexpected results. The expectations that do work in determining the due date from a timespan, do not calculate correctly when I back calculate them.
Can anyone see whether I am doing something wrong, or whether the library has a bug?
namespace ServicePlanner
{
using System;
using System.Collections.Generic;
using Itenso.TimePeriod;
public class ServicePlannerManager
{
public ServicePlannerManager(IEnumerable<WorkDay> workDays, IEnumerable<HolidayPeriod> holidays)
{
this.WorkDays = workDays;
this.Holidays = holidays;
}
public IEnumerable<WorkDay> WorkDays { get; set; }
public IEnumerable<HolidayPeriod> Holidays { get; set; }
public TimeSpan GetRemainingWorkingTime(DateTime start, DateTime dueDate)
{
var filter = new CalendarPeriodCollectorFilter();
foreach (var dayOfWeek in this.WorkDays)
{
filter.CollectingDayHours.Add(new DayHourRange(dayOfWeek.DayOfWeek, new Time(dayOfWeek.StartTime), new Time(dayOfWeek.EndTime)));
}
foreach (var holiday in this.Holidays)
{
filter.ExcludePeriods.Add(new TimeBlock(holiday.StartTime, holiday.EndTime));
}
var range = new CalendarTimeRange(start, dueDate);
var collector = new CalendarPeriodCollector(filter, range);
collector.CollectHours();
var duration = collector.Periods.GetTotalDuration(new TimeZoneDurationProvider(TimeZoneInfo.FindSystemTimeZoneById("UTC")));
return duration;
//var rounded = Math.Round(duration.TotalMinutes, MidpointRounding.AwayFromZero);
//return TimeSpan.FromMinutes(rounded);
}
}
}
The Unit tests that are failing are extracted below:
[TestFixture]
public class ServicePlannerManagerTest
{
[Test, TestCaseSource("LocalSource")]
public void GetRemainingWorkingTimeWithHolidayShouldOnlyEnumerateWorkingTime(DateTime startTime, TimeSpan workingHours, DateTime expectedDueDate, string expectation)
{
// Arrange
var workDays = new List<WorkDay>
{
new WorkDay(DayOfWeek.Monday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
new WorkDay(DayOfWeek.Tuesday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
new WorkDay(DayOfWeek.Wednesday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
new WorkDay(DayOfWeek.Thursday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
new WorkDay(DayOfWeek.Friday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
};
var holidayPeriods = new List<HolidayPeriod>
{
new HolidayPeriod(new DateTime(2015, 9, 15, 00, 0, 0), new DateTime(2015, 9, 16, 0, 0, 0))
};
var service = new ServicePlannerManager(workDays, holidayPeriods);
// Act
var result = service.GetRemainingWorkingTime(startTime, expectedDueDate);
// Assert -
Assert.AreEqual(workingHours.TotalHours, result.TotalHours, expectation);
}
protected IEnumerable LocalSource()
{
yield return
new TestCaseData(
new DateTime(2015, 9, 14, 9, 0, 0),
new TimeSpan(23, 0, 0),
new DateTime(2015, 9, 17, 16, 0, 0),
"5. Expected 23 hours of working time to end on the 17/09/2015 16:00. Monday to Thursday evening. Just short of 3 full working days by one hour. Tuesday is holiday.");
}
}
Output of this test is
5. Expected 23 hours of working time to end on the 17/09/2015 16:00. Monday to Thursday evening. Just short of 3 full working days by one hour. Tuesday is holiday.
Expected: 23.0d
But was: 15.999999999944444d
I want to know if I am using the collector incorrectly, or if the collector has a bug.
This looks like a great library for solving a familiar problem.
The best thing to do is to output the periods in the period collection to help you debug the problem.
I've rewritten your test to use the base types in the examples from their documentation:
[Test, TestCaseSource("LocalSource")]
public void SO_GetRemainingWorkingTimeWithHolidayShouldOnlyEnumerateWorkingTime(DateTime startTime,
TimeSpan workingHours, DateTime expectedDueDate, string expectation)
{
CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
filter.Months.Add(YearMonth.September); // only Januaries
filter.WeekDays.Add(DayOfWeek.Monday); //
filter.WeekDays.Add(DayOfWeek.Tuesday); //
filter.WeekDays.Add(DayOfWeek.Wednesday); //
filter.WeekDays.Add(DayOfWeek.Thursday); //
filter.WeekDays.Add(DayOfWeek.Friday); //
filter.CollectingHours.Add(new HourRange(9, 17)); // working hours
CalendarTimeRange testPeriod = new CalendarTimeRange(startTime, expectedDueDate);//new DateTime(2015, 9, 14, 9, 0, 0), new DateTime(2015, 9, 17, 18, 0, 0));
Console.WriteLine("Calendar period collector of period: " + testPeriod);
filter.ExcludePeriods.Add(new TimeBlock(new DateTime(2015, 9, 15, 00, 0, 0), new DateTime(2015, 9, 16, 0, 0, 0)));
CalendarPeriodCollector collector = new CalendarPeriodCollector(filter, testPeriod);
collector.CollectHours();
foreach (ITimePeriod period in collector.Periods)
{
Console.WriteLine("Period: " + period); // THIS WILL HELP A LOT!
}
var result = collector.Periods.GetTotalDuration(new TimeZoneDurationProvider(TimeZoneInfo.FindSystemTimeZoneById("UTC")));
Console.WriteLine(result);
//
}
This results in:
Calendar period collector of period: 14/09/2015 09:00:00 - 17/09/2015 15:59:59 | 3.06:59
Period: 14/09/2015 09:00:00 - 14/09/2015 16:59:59 | 0.07:59
Period: 16/09/2015 09:00:00 - 16/09/2015 16:59:59 | 0.07:59
15:59:59.9999998
So what I've noticed is that the very last period is missing.
If you change the end time of your period from 4PM to 6PM (and therefore expect an extra hour = 24) it will just about pass. (you will also need to round the result)
So it looks like the periods need to be completely covered by the total duration, partial coverage is not counted. You may be able to change the options of the library, alternatively you may be able to add each hour of the working day as separate CollectingHours (hacky)
Hope that gets you closer to the answer you need!
This question already has answers here:
Showing Difference between two datetime values in hours
(8 answers)
Closed 8 years ago.
I have a List of DateTimes
var times = CurrentMeter.SessionTimes.ToList();
How do I find the difference between them and add the results to a new List?
I want the hours, minutes, seconds and milliseconds between each datetime.
A difference between two DateTime objects is a TimeSpan.
This function will calculate the time delta between each consecutive pair of DateTimes.
It will iterate through each time in times, subtract the previous value from it, and add the difference to your result list.
IEnumerable<TimeSpan> CalculateDeltas(IEnumerable<DateTime> times) {
var time_deltas = new List<TimeSpan>();
DateTime prev = times.First();
foreach (var t in times.Skip(1)) {
time_deltas.Add(t - prev);
prev = t;
}
return time_deltas;
}
Here's an example to calculate the difference between each item in the list. The results are stored in a List<double> and represent the total of seconds between each datetime.
var times = new List<DateTime>
{
new DateTime(2014, 5, 28, 9, 57, 12),
new DateTime(2014, 5, 28, 9, 57, 43),
new DateTime(2014, 5, 28, 9, 58, 03),
new DateTime(2014, 5, 28, 9, 59, 46),
new DateTime(2014, 5, 28, 10, 0, 22),
};
var differences = new List<double>();
for(int i = 0; i < times.Count - 1; i++)
{
differences.Add((times[i+1] - times[i]).TotalSeconds);
}
Output:
31
20
103
36
How about:
var time = new []{
DateTime.Parse("14-4-2012 00:00:00"),
DateTime.Parse("14-4-2012 01:12:34"),
DateTime.Parse("14-4-2012 12:44:33"),
DateTime.Parse("14-4-2012 23:12:42"),
};
var result = time.Zip(time.Skip(1), (a, b) => b - a)
.Select(d => new {d.Hours, d.Minutes, d.Seconds})
.ToList();
result is now:
(Note that this is a very simply approach and only works for differences < 24h. If you have greater differences, take into account d.Days etc.)
Lets say I 2 longs a start and end, which are really two date times converted to ticks. How would I tell if these two values overlap?
As MPelletier mentioned: you need two pairs of DateTimes:
var start1 = new DateTime(2013, 4, 10).Ticks;
var end1 = new DateTime(2013, 4, 20).Ticks;
var start2 = new DateTime(2013, 4, 9).Ticks;
var end2 = new DateTime(2013, 4, 11).Ticks;
if (start2 < end1)
{
//// Overlapping
}