Find distinct TimeSpan duration from a list of TimeSpans - c#

I'm having a bit of trouble trying to process a list of TimeSpan objects without having a lot of code which still doesn't seem to cover all eventualities, tbh, I think I've gone a bit code/logic blind now!
I have a list of TimeSpans of which overlaps will likely occur, but I need a list of TimeSpans which have no overlaps, but cover the whole duration of all TimeSpans.
For example (please note, dates are in ddMMyyyy format):
TS1: 01/01/2020 to 01/02/2020 (1 month)
TS2: 01/03/2020 to 01/05/2020 (2 months)
TS3: 01/04/2020 to 01/07/2020 (3 months with a 1 month overlap with TS2)
TS4: 01/10/2020 to 01/12/2020 (2 months)
TS5: 01/09/2020 to 01/01/2021 (4 months with a 2 month overlap with TS4)
So in this case I would expect to get 3 TimeSpans:
TSA: 01/01/2020 to 01/02/2020 (1 month - same as TS1 as there are no overlaps)
TSB: 01/03/2020 to 01/07/2020 (4 months - combination of TS2 and TS3)
TSC: 01/09/2020 to 01/01/2021 (4 months - combination of TS4 and TS5, technically only TS5 as TS4 is fully encompassed by TS5)
I've tried researching an algorithm online, but without any luck.
Any suggestions would be very welcome.

This isn't optimized at all, but semantically you can do this by adding the chunks and looking for overlaps, then merging those overlaps; something like:
using System;
using System.Collections.Generic;
using System.Globalization;
static class P
{
static void Main()
{
var results = new List<(DateTime From, DateTime To)>();
Add("01/01/2020", "01/02/2020");
Add("01/03/2020", "01/05/2020");
Add("01/04/2020", "01/07/2020");
Add("01/10/2020", "01/12/2020");
Add("01/09/2020", "01/01/2021");
// SEE BELOW, IMPORTANT
results.Sort(); // initial sort
while (MergeOneOverlap()) { }
foreach (var range in results)
{
Console.WriteLine($"{range.From:dd/MM/yyyy} - {range.To:dd/MM/yyyy}");
}
bool MergeOneOverlap()
{
for (int i = 0; i < results.Count; i++)
{
var x = results[i];
for (int j = i + 1; j < results.Count; j++)
{
var y = results[j];
if (x.Intersects(y))
{
results[i] = x.Merge(y);
results.RemoveAt(j);
results.Sort(); // retain sort while making progress
return true;
}
}
}
return false;
}
void Add(string from, string to)
=> results.Add(
(DateTime.ParseExact(from, "dd/MM/yyyy", CultureInfo.InvariantCulture),
DateTime.ParseExact(to, "dd/MM/yyyy", CultureInfo.InvariantCulture)));
}
static bool ContainsInclusive(this (DateTime From, DateTime To) range, DateTime when)
=> when >= range.From && when <= range.To;
static bool Intersects(this (DateTime From, DateTime To) x, (DateTime From, DateTime To) y)
=> x.ContainsInclusive(y.From) || x.ContainsInclusive(y.To) || y.ContainsInclusive(x.From) || y.ContainsInclusive(x.To);
static (DateTime From, DateTime To) Merge(this (DateTime From, DateTime To) x, (DateTime From, DateTime To) y)
=> (x.From < y.From ? x.From : y.From, x.To > y.To ? x.To : y.To);
}
If this is for large amounts of data, you'd have to look into being much smarter to avoid an O(N^3) problem. It may help to merge every add, if that will often keep the number of items down.
It may also be possible to reduce the complexity to O(N^2) and merging purely forwards (i.e. don't break on successful merge), but I haven't applied enough thinking to see about the implications of that. And O(N^2) is still pretty bad.
For large data, using a sorted list may help, so you can do a binary search on the start dates to find the insertion point. That is getting more complex than I care to write here, though.
I'm 95% sure that this is also fine, i.e. O(N^2):
MergeOverlaps();
foreach (var range in results)
{
Console.WriteLine($"{range.From:dd/MM/yyyy} - {range.To:dd/MM/yyyy}");
}
void MergeOverlaps()
{
results.Sort();
for (int i = 0; i < results.Count; i++)
{
var x = results[i];
for (int j = i + 1; j < results.Count; j++)
{
var y = results[j];
if (x.Intersects(y))
{
results[i] = x = x.Merge(y);
results.RemoveAt(j--);
}
}
}
}

I would suggest to try a brute force search or a depth-first search algorithm.
First you sort the timespans by starting date.
BRUTE FORCE:
You try all combinations and score them by overlap/not overlap, and you probably want to score them by how much of the total timespan is covered.
DEPTH-FIRST-SEARCH:
Write a recursive algorithm that start by adding the first interval and then add more interval and backtracks whenever an overlap occur.

Related

Extracting dates of current week using .Net?

I am trying to extract dates for current week's days and I just can't find a sensible, smart way instead of a long case, switches and if statements.
Anybody knows a relatively easy way to extract those using .Net?
Thanks!
The DateTime.DayOfWeek is an enumeration that starts with Sunday being 0 and going forward. If you take today's day-of-week, it will also tell how many days ago Sunday was. Therefore going back that many days will give you the Sunday of this week, assuming week starts on Sunday. You can go forward from that for the seven days of the week.
var today = DateTime.Now;
var thisSunday = today.AddDays(-(int)today.DayOfWeek);
for (int i=0; i<7; i++)
Console.WriteLine(thisSunday.AddDays(i).ToString());
If the week starts from Monday, use
var thisMonday = today.AddDays(-(((int)today.DayOfWeek + 6) % 7));
You may use extension method to set the day that week start with (credit goes to #Compile This)
public static class DateTimeExtensions
{
public static DateTime StartOfWeek(this DateTime datetime, DayOfWeek startOfWeek)
{
int difference = datetime.DayOfWeek - startOfWeek;
if (difference >= 0)
return datetime.AddDays(-1 * difference).Date;
difference += 7;
return datetime.AddDays(-1 * difference).Date;
}
}
Then you can get date of the week using same loop as #Sami_Kuhmonen mentioned:
DateTime d = DateTime.Now.StartOfWeek(DayOfWeek.Saturday);
for (int i = 0; i < 7; i++)
Console.WriteLine(d.AddDays(i));

Calculate Monday-Sunday week from a DateTime object

I'm trying to write a method that will return a list of DateTimes representing a Monday-Sunday week. Its supposed to take the DateTime given to it and use it to calculate the surrounding dates
It calculates the starting date ok but the problem begins when it hits the last loop. With each run through the DateTime variable tmpDate should be incremented by 1 day, then added to the list. However, as it stands I'm getting back a List containing 7 starting dates.
Can anyone see where I'm going wrong (I have the feeling I may be made to look like a bit of a simpleton on this :) )?
Also, apologies if this is an often-asked question. Could see plenty of Start date/End Date and week number type questions, but none specifically dealing with this type of problem.
private List<DateTime> getWeek(DateTime enteredDate)
{
/* Create List to hold the dates */
List<DateTime> week = new List<DateTime>();
int enteredDatePosition = (int)enteredDate.DayOfWeek;
/* Determine first day of the week */
int difference = 0;
for (int i = 0; i < 7; i++)
{
difference++;
if (i == enteredDatePosition)
{
break;
}
}
// 2 subtracted from difference so first and enteredDatePostion elements will not be counted.
difference -= 2;
DateTime startDate = enteredDate.Subtract(new TimeSpan(difference, 0, 0, 0));
week.Add(startDate);
/* Loop through length of a week, incrementing date & adding to list with each iteration */
DateTime tmpDate = startDate;
for (int i = 1; i < 7; i++)
{
tmpDate.Add(new TimeSpan(1, 0, 0, 0));
week.Add(tmpDate);
}
return week;
}
DateTimes are immutable.
tmpDate.Add(...) returns a new DateTime, and does not modify tmpDate.
You should write tmpDate = tmpDate.AddDays(1) or tmpDate += TimeSpan.FromDays(1)
I believe this snippet is tailored towards Sunday trough Saturday, but you can try something like this:
DateTime ToDate = DateTime.Now.AddDays((6 - (int)DateTime.Now.DayOfWeek) - 7);
DateTime FromDate = ToDate.AddDays(-6);
You're making your algorithm more complex than it needs to be. Check out this working snippet:
using System;
using System.Collections.Generic;
public class MyClass
{
public static void Main()
{
// client code with semi-arbitrary DateTime suuplied
List<DateTime> week = GetWeek(DateTime.Today.AddDays(-2));
foreach (DateTime dt in week) Console.WriteLine(dt);
}
public static List<DateTime> GetWeek(DateTime initDt)
{
// walk back from supplied date until we get Monday and make
// that the initial day
while (initDt.DayOfWeek != DayOfWeek.Monday)
initDt = initDt.AddDays(-1.0);
List<DateTime> week = new List<DateTime>();
// now enter the initial day into the collection and increment
// and repeat seven total times to get the full week
for (int i=0; i<7; i++)
{
week.Add(initDt);
initDt = initDt.AddDays(1.0);
}
return week;
}
}

searching results of ordered LINQ query

my LINQ query returns an ordered sequence of calendar dates, and i need to output this sequence starting from the earliest date that is more that the given number of days apart from the starting date of the sequence.
the code below does that using linear search. it seems that i could use a binary search to find the beginning date if LINQ query supported this.
in this contrived example i can search the list but in my real code i am trying to avoid storing the whole sequence in memory and i prefer to use just IEnumerable.
any ideas how to make it more efficient? i have thousands of items in my query and doing linear search is just lame...
thanks
konstantin
using System;
using System.Collections.Generic;
using System.Linq;
namespace consapp
{
static class Program
{
static void Main(string[] args)
{
var dates = new List<DateTime>();
var xs = dates.OrderBy(x => x);
dates.Add(DateTime.Parse("11/10/11"));
dates.Add(DateTime.Parse("02/02/11"));
dates.Add(DateTime.Parse("11/24/11"));
dates.Add(DateTime.Parse("09/09/11"));
dates.Add(DateTime.Parse("11/10/11"));
var d = DateTime.MinValue;
double offset = 1.2;
foreach (var x in xs)
{
if (d != DateTime.MinValue)
{
offset -= (x - d).Days;
}
if (offset < 1)
{
Console.WriteLine(x.ToShortDateString());
}
d = x;
}
}
}
}
Binary search would likely be better if your data set is pre-sorted or you do not know the start date of your sequence ahead of time. However if you are sorting your dates using OrderBy like your example and you do know the start date of the sequence, why not put a Where clause in to filter out the dates that don't meet your criteria before you order the sequence?
var xs = from date in dates
where (date - target).Days < 1.2
order by date
select date;
If you have sorted dates IEnumerable in sortedData collection, then here how you can get select dates later than a threshold from the first date:
var threshold = TimeSpan.FromDays(1);
var filteredDates = sortedDates.SkipWhile(sd => sd - sortedDates.First() <= theshold);
It has the advantage over .Where that it only need to go check first dates till it gets to threshold. After that it's just enumerating elements.
Note that it's IEnumerate, so you get all the benefits of lazy loading

How can I get correct payperiod from date?

I feel like this is math problem more than anything. My company has employees all over the country. Some parts of the company are on an "odd" pay cycle and some are on "even". I call the starting date of a given pay period a "payperiod". I need to do two things:
1) determine the payperiod in which a given date falls
//Something like this:
public static DateTime getPayPeriodStartDate(DateTime givenDate, string EvenOrOdd)
{ .. }
2) get a list of payperiods between two dates:
//Something like this:
public static List<DateTime> getPayPeriodsBetween(DateTime start, DateTime end, string EvenOrOdd)
{ .. }
I'm using a couple dates as fixed standards on which to base any future pay period dates. The fixed standard dates for even and odd are as follows:
Even - 01/04/09
Odd - 01/11/09
Each pay period starts on the sunday of the week and goes for two weeks. For instance, using the standard dates above, the first even pay period starts on 01/04/09 and ends on 01/17/09. The first odd pay period starts on 01/11/09 and ends on 01/24/09. As you can see, there is some overlap. We have thousands of employees so it's necessary to split them up a bit.
I have a solution that is based on week numbers but it's clunky and has to be "fixed" every new year. I'm wondering how you would handle this.
Not fully optimized or tested, but this is what I came up with:
const int DaysInPeriod = 14;
static IEnumerable<DateTime> GetPayPeriodsInRange(DateTime start, DateTime end, bool isOdd)
{
var epoch = isOdd ? new DateTime(2009, 11, 1) : new DateTime(2009, 4, 1);
var periodsTilStart = Math.Floor(((start - epoch).TotalDays) / DaysInPeriod);
var next = epoch.AddDays(periodsTilStart * DaysInPeriod);
if (next < start) next = next.AddDays(DaysInPeriod);
while (next <= end)
{
yield return next;
next = next.AddDays(DaysInPeriod);
}
yield break;
}
static DateTime GetPayPeriodStartDate(DateTime givenDate, bool isOdd)
{
var candidatePeriods = GetPayPeriodsInRange(givenDate.AddDays(-DaysInPeriod), givenDate.AddDays(DaysInPeriod), isOdd);
var period = from p in candidatePeriods where (p <= givenDate) && (givenDate < p.AddDays(DaysInPeriod)) select p;
return period.First();
}
I haven't tested for many test cases, but I think this fits the bill:
public static DateTime getPayPeriodStartDate(DateTime givenDate, string EvenOrOdd)
{
DateTime newYearsDay = new DateTime(DateTime.Today.Year, 1, 1);
DateTime firstEvenMonday = newYearsDay.AddDays((8 - (int)newYearsDay.DayOfWeek) % 7);
DateTime firstOddMonday = firstEvenMonday.AddDays(7);
TimeSpan span = givenDate - (EvenOrOdd.Equals("Even") ? firstEvenMonday : firstOddMonday);
int numberOfPayPeriodsPast = span.Days / 14;
return (EvenOrOdd.Equals("Even") ? firstEvenMonday : firstOddMonday).AddDays(14 * numberOfPayPeriodsPast);
}
public static List<DateTime> getPayPeriodsBetween(DateTime start, DateTime end, string EvenOrOdd)
{
DateTime currentPayPeriod = getPayPeriodStartDate(start, EvenOrOdd);
if (currentPayPeriod < start) currentPayPeriod = currentPayPeriod.AddDays(14);
List<DateTime> dtList = new List<DateTime>();
while (currentPayPeriod <= end)
{
dtList.Add(currentPayPeriod);
currentPayPeriod = currentPayPeriod.AddDays(14);
}
return dtList;
}
I am sure it can be improved.
I had a need to do something similar and was able to do it very easily using LINQ. Simply build up a List for even and odd and then query between dates from the odd/even as necessary. Also, I recommend you move to an emum for parameters like EvenOrOdd where you have fixed values.
I had a similar problem a few months ago, and I ended up writing a quick script to create entries in a database for each pay period so I never had to actually do the math. This way, The system works the same speed, and doesn't have to do any slow iterations every time a period is requested.
That being said, you can always take the starting date, and add two weeks (or however long your periods are) over and over until you reach the dates you specify in the function call. This is a bit ugly, and the longer it sits in production, the slower it gets (since the dates are getting further and further apart).
Both ways are trivial to implement, it's just a matter of what kind of resources you have at hand to tackle the issue.
So, for number 1: Start with either 1/4/2009 or 1/11/2009 (depending on even/odd pay week) and add 2 weeks until the givenDate is less than the date you're testing + 2 weeks. That's the start of the period.
For number 2: Same thing, start at the date and add 2 weeks until you're within the date range. While you're there, add each item to a list. As soon as you're past the last date, break out of your loop and return your shiny new list.
If you used my method and went with a database to house all this info, it turns into 2 simple queries:
1)SELECT * FROM payperiods WHERE startdate<=givenDate ORDER BY startdate LIMIT 1
2) SELECT * FROM payperiods WHERE startdate>=givenDate AND enddate<=givenDate ORDER BY startdate
It works perfectly. I have tested.
public static DateTime GetFirstDayOfWeek(DateTime dayInWeek)
{
CultureInfo _culture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
CultureInfo _uiculture = (CultureInfo)CultureInfo.CurrentUICulture.Clone();
_culture.DateTimeFormat.FirstDayOfWeek = DayOfWeek.Monday;
_uiculture.DateTimeFormat.FirstDayOfWeek = DayOfWeek.Monday;
System.Threading.Thread.CurrentThread.CurrentCulture = _culture;
System.Threading.Thread.CurrentThread.CurrentUICulture = _uiculture;
// CultureInfo defaultCultureInfo = CultureInfo.CurrentCulture;
DayOfWeek firstDay = _culture.DateTimeFormat.FirstDayOfWeek;
DateTime firstDayInWeek = dayInWeek.Date;
// Logic Of getting pay period Monday(Odd monday)
int i = Convert.ToInt32(firstDay);
while (firstDayInWeek.DayOfWeek != firstDay)
if (i % 2 != 0)
{ firstDayInWeek = firstDayInWeek.AddDays(-1); }
else
{
firstDayInWeek = firstDayInWeek.AddDays(-2);
}
return firstDayInWeek;
}

Clean way to reduce many TimeSpans into fewer, average TimeSpans?

I have a C# Queue<TimeSpan> containing 500 elements.
I need to reduce those into 50 elements by taking groups of 10 TimeSpans and selecting their average.
Is there a clean way to do this? I'm thinking LINQ will help, but I can't figure out a clean way. Any ideas?
I would use the Chunk function and a loop.
foreach(var set in source.ToList().Chunk(10)){
target.Enqueue(TimeSpan.FromMilliseconds(
set.Average(t => t.TotalMilliseconds)));
}
Chunk is part of my standard helper library.
http://clrextensions.codeplex.com/
Source for Chunk
Take a look at the .Skip() and .Take() extension methods to partition your queue into sets. You can then use .Average(t => t.Ticks) to get the new TimeSpan that represents the average. Just jam each of those 50 averages into a new Queue and you are good to go.
Queue<TimeSpan> allTimeSpans = GetQueueOfTimeSpans();
Queue<TimeSpan> averages = New Queue<TimeSpan>(50);
int partitionSize = 10;
for (int i = 0; i <50; i++) {
var avg = allTimeSpans.Skip(i * partitionSize).Take(partitionSize).Average(t => t.Ticks)
averages.Enqueue(new TimeSpan(avg));
}
I'm a VB.NET guy, so there may be some syntax that isn't 100% write in that example. Let me know and I'll fix it!
Probably nothing beats a good old procedural execution in a method call in this case. It's not fancy, but it's easy, and it can be maintained by Jr. level devs.
public static Queue<TimeSpan> CompressTimeSpan(Queue<TimeSpan> original, int interval)
{
Queue<TimeSpan> newQueue = new Queue<TimeSpan>();
if (original.Count == 0) return newQueue;
int current = 0;
TimeSpan runningTotal = TimeSpan.Zero;
TimeSpan currentTimeSpan = original.Dequeue();
while (original.Count > 0 && current < interval)
{
runningTotal += currentTimeSpan;
if (++current >= interval)
{
newQueue.Enqueue(TimeSpan.FromTicks(runningTotal.Ticks / interval));
runningTotal = TimeSpan.Zero;
current = 0;
}
currentTimeSpan = original.Dequeue();
}
if (current > 0)
newQueue.Enqueue(TimeSpan.FromTicks(runningTotal.Ticks / current));
return newQueue;
}
You could just use
static public TimeSpan[] Reduce(TimeSpan[] spans, int blockLength)
{
TimeSpan[] avgSpan = new TimeSpan[original.Count / blockLength];
int currentIndex = 0;
for (int outputIndex = 0;
outputIndex < avgSpan.Length;
outputIndex++)
{
long totalTicks = 0;
for (int sampleIndex = 0; sampleIndex < blockLength; sampleIndex++)
{
totalTicks += spans[currentIndex].Ticks;
currentIndex++;
}
avgSpan[outputIndex] =
TimeSpan.FromTicks(totalTicks / blockLength);
}
return avgSpan;
}
It's a little more verbose (it doesn't use LINQ), but it's pretty easy to see what it's doing... (you can a Queue to/from an array pretty easily)
I'd use a loop, but just for fun:
IEnumerable<TimeSpan> AverageClumps(Queue<TimeSpan> lots, int clumpSize)
{
while (lots.Any())
{
var portion = Math.Min(clumpSize, lots.Count);
yield return Enumerable.Range(1, portion).Aggregate(TimeSpan.Zero,
(t, x) => t.Add(lots.Dequeue()),
(t) => new TimeSpan(t.Ticks / portion));
}
}
}
That only examines each element once, so the performance is a lot better than the other LINQ offerings. Unfortunately, it mutates the queue, but maybe it's a feature and not a bug?
It does have the nice bonus of being an iterator, so it gives you the averages one at a time.
Zipping it together with the integers (0..n) and grouping by the sequence number div 10?
I'm not a linq user, but I believe it would look something like this:
for (n,item) from Enumerable.Range(0, queue.length).zip(queue) group by n/10
The take(10) solution is probably better.
How is the grouping going to be performed?
Assuming something very simple (take 10 at a time ), you can start with something like:
List<TimeSpan> input = Enumerable.Range(0, 500)
.Select(i => new TimeSpan(0, 0, i))
.ToList();
var res = input.Select((t, i) => new { time=t.Ticks, index=i })
.GroupBy(v => v.index / 10, v => v.time)
.Select(g => new TimeSpan((long)g.Average()));
int n = 0;
foreach (var t in res) {
Console.WriteLine("{0,3}: {1}", ++n, t);
}
Notes:
Overload of Select to get the index, then use this and integer division pick up groups of 10. Could use modulus to take every 10th element into one group, every 10th+1 into another, ...
The result of the grouping is a sequence of enumerations with a Key property. But just need those separate sequences here.
There is no Enumerable.Average overload for IEnumerable<TimeSpan> so use Ticks (a long).
EDIT: Take groups of 10 to fit better with question.
EDIT2: Now with tested code.

Categories