C# calculate date ranges from a list of dates - c#

Given a list of dates (which may not be sorted), I want to build a list of date ranges -
E.g. Assuming MM/DD format,
Input - 5/1, 5/5, 5/6, 5/15, 5/7, 5/8, 5/19,5/20, 5/23
Output -
Date Range 1: 5/1 to 5/1
Date Range 2: 5/5 to 5/8
Date Range 3: 5/15 to 5/15
Date Range 4: 5/19 to 5/20
Date Range 5: 5/23 to 5/23
Basically, a range should be continuous.

Sort the dates
Start a range containing the next date (to start with it will be the first one)
Is the second "valid" date the next date which would be in the range? If so, keep going. If not, close the current range and start a new one.
Repeat until you've run out of dates, at which point you close the current range and you're done.

You can create a list of DateTime (possibly using the same year for them all) and sort it.
It is then fairly easy to find if a day and the next day exist in the list (using DateTime.AddDays(1)).

public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
[TestClass]
public class DateRangerTest
{
private List<DateRange> GetDateRanges(List<DateTime> dates)
{
if (dates == null || !dates.Any()) return null;
dates = dates.OrderBy(x => x.Date).ToList();
var dateRangeList = new List<DateRange>();
DateRange dateRange = null;
for (var i = 0; i < dates.Count; i++)
{
if (dateRange == null)
{
dateRange = new DateRange { Start = dates[i] };
}
if (i == dates.Count - 1 || dates[i].Date.AddDays(1) != dates[i + 1].Date)
{
dateRange.End = dates[i].Date;
dateRangeList.Add(dateRange);
dateRange = null;
}
}
return dateRangeList;
}
[TestMethod]
public void GetDateRanges_MultiDateRangeTest()
{
var dates = new List<DateTime>
{
new DateTime(1999,5,1),
new DateTime(1999,5,5),
new DateTime(1999,5,6),
new DateTime(1999,5,15),
new DateTime(1999,5,7),
new DateTime(1999,5,8),
new DateTime(1999,5,19),
new DateTime(1999,5,20),
new DateTime(1999,5,23)
};
var dateRanges = GetDateRanges(dates);
Assert.AreEqual(new DateTime(1999, 5, 1), dateRanges[0].Start);
Assert.AreEqual(new DateTime(1999, 5, 1), dateRanges[0].End);
Assert.AreEqual(new DateTime(1999, 5, 5), dateRanges[1].Start);
Assert.AreEqual(new DateTime(1999, 5, 8), dateRanges[1].End);
Assert.AreEqual(new DateTime(1999, 5, 15), dateRanges[2].Start);
Assert.AreEqual(new DateTime(1999, 5, 15), dateRanges[2].End);
Assert.AreEqual(new DateTime(1999, 5, 19), dateRanges[3].Start);
Assert.AreEqual(new DateTime(1999, 5, 20), dateRanges[3].End);
Assert.AreEqual(new DateTime(1999, 5, 23), dateRanges[4].Start);
Assert.AreEqual(new DateTime(1999, 5, 23), dateRanges[4].End);
}
}

Related

Generate a list of weekly business dates between two dates excluding weekends and holidays

I want to generate a list of weekly business dates between two dates excluding weekends and holidays. I have managed to exclude the Weekends and created a routine for the holidays. Now I need to exclude the holidays. Below is my code.
using System;
using System.Collections.Generic;
using System.Linq;
namespace SeriesTest
{
class Program
{
public class BusinessWeekDays
{
public DateTime Monday;
public DateTime Sunday;
}
private static List<DateTime> Holidays = new List<DateTime>()
{
new DateTime(1, 1, 1), //New Year Day
new DateTime(1, 5, 1), //Labour Day
new DateTime(1, 7, 4), //Independence Day
new DateTime(1, 3, 1), //Martin Luther King Jr. Day
new DateTime(1, 3, 2), //Presidents Day
new DateTime(1, 12, 25), //Christmas
new DateTime(1, 5, 5), //Memorial Day
new DateTime(1, 9, 1), //Labor Day
new DateTime(1, 10, 2), //Columbus Day
new DateTime(1, 11, 4), //Columbus Day
};
private static bool IsHoliday(DateTime value, List<DateTime> holidays = null)
{
if (null == holidays)
holidays = Holidays;
return (value.DayOfWeek == DayOfWeek.Sunday) ||
(value.DayOfWeek == DayOfWeek.Saturday) ||
holidays.Any(holiday => holiday.Day == value.Day &&
holiday.Month == value.Month);
}
public static int BusinessDays(DateTime fromDate, DateTime toDate, List<DateTime> holidays = null)
{
int result = 0;
for (var date = fromDate;
date < toDate.Date;
date = date.AddDays(1))
if (!IsHoliday(date, holidays))
result += 1;
return result;
}
static void Main(string[] args)
{
var StartDate = DateTime.Parse("02/12/2019");
var SeriesEndDate = DateTime.Parse("12/31/2025");
var holidays = new List<DateTime>();
var firstMonday = Enumerable.Range(0, 7)
.SkipWhile(x => StartDate.AddDays(x).DayOfWeek != DayOfWeek.Monday)
.Select(x => StartDate.AddDays(x))
.First();
var ts = (SeriesEndDate - firstMonday);
var dates = new List<BusinessWeekDays>();
for (var i = 0; i < ts.Days; i += 7)
{
//Remove holidays. Weekend already removed here
if (BusinessDays(StartDate, SeriesEndDate, holidays) != 0)
{
dates.Add(new BusinessWeekDays { Monday = firstMonday.AddDays(i), Sunday = firstMonday.AddDays(i + 9) });
}
}
Console.WriteLine(dates);
}
}
}
It looks like you already have everything you need, but holidays is never set to your Holidays. Change the line below so that holidays is set to null. Then it will hit the null check in IsHoliday and be set properly.
var holidays = new List<DateTime>();
Should be:
var holidays = null;

Insert Data into middle of range

I have the following model
public class DailyRoutine
{
public int Id { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
}
Scenario:
When is created at initial time with 5 records which means 5 entries are entered for each day. Take an example May 1 to May 5 of 2017. Description have any string.
User can add a new record in the middle so that the following records should be moved and changed to next days.
Expected Output:
Example, user can give a date and description in input and submit. If the input date is '5/3/2017' (May 3), the entry should be added after May 2 record and the existing May 3 record changed to May 4, May 4 to May 5 etc. So the out is like May 1 to May 6 and the given input is updated on May 3.
Please help me to this with out degrading performance
This approach will work:
List<DailyRoutine> d = new List<DailyRoutine>()
{
new DailyRoutine() { Date = new DateTime(2017, 7, 1)},
new DailyRoutine() { Date = new DateTime(2017, 7, 2)},
new DailyRoutine() { Date = new DateTime(2017, 7, 3)},
new DailyRoutine() { Date = new DateTime(2017, 7, 4)},
new DailyRoutine() { Date = new DateTime(2017, 7, 5)}
};
DailyRoutine newDr = new DailyRoutine() { Date = new DateTime(2017, 7, 2) };
DailyRoutine oldDr = d.Where(dr => dr.Date == newDr.Date).FirstOrDefault();
if (oldDr != null)
{
int idx = d.IndexOf(oldDr);
List<DailyRoutine> changeList = d.Where((dr, i) => i >= idx).ToList();
foreach (DailyRoutine i in changeList)
{
i.Date = i.Date.AddDays(1);
}
d.Insert((int)idx, newDr);
}
else
{
d.Add(newDr);
}

Removing Overlapping Dates in LINQ

I have a really ghetto implementation of this, but I imagine there is some clean way to do this with Linq. I have a list of objects with a start and stop date.
class Thing
{
int ID;
...
DateTime? StartDate;
DateTime EndDate;
}
In some cases there is no start date, its null. What I want the algorithm to do is remove items from the list until there is no overlapping of dates; I'm just using years to illustrate the concept:
List<Thing> x = new List<Thing>()
{
{1, 2012, 2014}
{2, 2013, 2015}
{3, 2014, 2016}
{4, <null>, 2015}
{5, 2016, 2017}
}
Running this list through the algo should yield:
var y = Do(x);
y =
{
{1, 2012, 2014}
{2, <null>, 2015}
{3, 2016, 2017}
}
There is the possibilities for cycles where, where there are different optimal solutions. My data doesn't have these edge cases.
I think this should work for you:
List<Thing> x = new List<Thing>()
{
new Thing () { ID = 1, StartDate = new DateTime(2012, 1, 1), EndDate = new DateTime(2014, 1, 1) },
new Thing () { ID = 2, StartDate = new DateTime(2013, 1, 1), EndDate = new DateTime(2015, 1, 1) },
new Thing () { ID = 3, StartDate = new DateTime(2014, 1, 1), EndDate = new DateTime(2016, 1, 1) },
new Thing () { ID = 4, StartDate = null, EndDate = new DateTime(2015, 1, 1) },
new Thing () { ID = 5, StartDate = new DateTime(2016, 1, 1), EndDate = new DateTime(2017, 1, 1) },
};
Func<Thing, Thing, bool> overlaps =
(t0, t1) =>
(t0.StartDate.HasValue ? t0.StartDate.Value <= t1.EndDate : false)
&& (t1.StartDate.HasValue ? t0.EndDate >= t1.StartDate : false);
var y = x.Skip(1).Aggregate(x.Take(1).ToList(), (a, b) =>
{
if (a.All(c => !overlaps(b, c)))
{
a.Add(b);
}
return a;
});
This gives me:
If your class Thing implements IEquatable<Thing> and you properly fill out the Equals, and GetHashCode methods you can then just do
var results = x.Distinct();

A better way to fill a two-dimensional array with date and zodiac sign

I'm working on the following problem:
I want to fill a two-dimensional [365,2] Array. The first value is supposed to hold the date: starting with January 1st and ending with December the 31st. The second value is supposed to hold the corresponding Zodiac Sign to each date:
e.g. array[0, 0] holds 101 and array[0, 1] holds Aries and so on.
I've written a function:
public static void fill_array(string[,] year_zodiac, int days, string zodiac, string startdate, int starting_day)
{
//function to fill array with date and zodiac
int startdate_int = 0;
for (int i = starting_day; i <= days; i++)
{
year_zodiac[i, 0] = startdate;
year_zodiac[i, 1] = zodiac;
startdate_int = Int32.Parse(startdate);
startdate_int++;
startdate = startdate_int.ToString();
}
and call it like this:
fill_array(main_array, 18, "Aries", "101", 0);
This has to be done for every Zodiac sign. I circumvented the month break problem by simply calling fill_array twice (i.e. I call it once for the part of Aries in December and once for the part of aries in January).
This Solution works, but it seems incredibly crude to me.
Can anybody give me some pointers towards a more elegant solution?
Here is a class that does what you want, it has been tested. I did not fill out all signs on the first example, but when I re-factored it I did. I suggest you test this well as I only tested a few cases and might have missed some edge cases.
As has been pointed out to me by #ClickRick, I did start with his array/enum design, but found that lacking when using Linq and moved to a List. I also had to fix his data array so it would compile. I'm giving credit as is due.
public class Signs
{
static List<string> SignList = new List<string>() { "Aquarius", "Pisces", "Aries", "Taurus", "Not Found"};
static DateTime[] startDates = {
new DateTime(DateTime.Now.Year,1,21),
new DateTime(DateTime.Now.Year,2,20),
new DateTime(DateTime.Now.Year,3,21),
new DateTime(DateTime.Now.Year,4,21),
new DateTime(DateTime.Now.Year,12,31) };
static public string Sign(DateTime inDate)
{
return SignList.Zip(startDates, (s, d) => new { sign = s, date = d })
.Where(x => (inDate.Month*100)+inDate.Day <= (x.date.Month*100)+x.date.Day)
.Select(x => x.sign)
.First();
}
}
re-factored (this is clearer with the example above first)
public class Signs
{
static List<string> SignList = new List<string>() {
"Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Not Found" };
static List<int> startDates = new List<int>() {
// month * 100 + day of month
120, 219, 320, 420, 521, 621, 722, 821, 923, 1023, 1122, 1222, 1232, 9999 // edge marker
};
static public string Sign(DateTime inDate)
{
return SignList[startDates.TakeWhile(d => (inDate.Month*100)+inDate.Day > d).Count()];
}
}
Why do you specifically want an array? Surely a more sophisticated data structure would help to encapsulate the functionality and help you to isolate your remaining code from knowing about the workings of it, and would also let you take account in the future of more subtle variations in the exact dates where they vary by year if you ever want to do that.
For example:
public class SO23182879
{
public enum StarSign
{
Aquarius, Pisces, Aries, Taurus, Gemini, Cancer,
Leo, Virgo, Libra, Scorpio, Sagittarius, Capricorn
};
static DateTime[] starSignStartDates = new DateTime[]
{
new DateTime(DateTime.Now.Year, 1, 20),
new DateTime(DateTime.Now.Year, 2, 19),
new DateTime(DateTime.Now.Year, 3, 21),
new DateTime(DateTime.Now.Year, 4, 20),
new DateTime(DateTime.Now.Year, 5, 21),
new DateTime(DateTime.Now.Year, 6, 21),
new DateTime(DateTime.Now.Year, 7, 23),
new DateTime(DateTime.Now.Year, 8, 23),
new DateTime(DateTime.Now.Year, 9, 23),
new DateTime(DateTime.Now.Year, 10, 23),
new DateTime(DateTime.Now.Year, 11, 22),
new DateTime(DateTime.Now.Year, 12, 22),
new DateTime(DateTime.Now.Year, 1, 20),
};
private class StarSignDateRange
{
public StarSign Sign { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
private List<StarSignDateRange> signStartDates = new List<StarSignDateRange>();
public SO23182879()
{
int date = 0;
foreach (StarSign sign in Enum.GetValues(typeof(StarSign)))
{
signStartDates.Add(new StarSignDateRange
{
Sign = sign,
StartDate = starSignStartDates[date],
EndDate = starSignStartDates[date + 1]
});
++date;
}
}
public StarSign Sign(DateTime date)
{
return signStartDates.First(
sd => date.Month == sd.StartDate.Month && date.Day >= sd.StartDate.Day ||
date.Month == sd.EndDate.Month && date.Day < sd.EndDate.Day
).Sign;
}
public void Test()
{
IList<DateTime> testDates = new List<DateTime>
{
new DateTime(2014,1,1),
new DateTime(2014,1,19),
new DateTime(2014,1,20),
new DateTime(2014,4,19),
new DateTime(2014,4,20),
new DateTime(2014,12,21),
new DateTime(2014,12,22),
new DateTime(2014,12,31),
};
foreach (DateTime d in testDates)
Console.WriteLine(string.Format("{0} is in {1}", d, Sign(d)));
}
}
As you'll see, I have completed the code which I had started earlier, and added a Test() method for you to see that the edge conditions work. I am grateful to Hogan for pointing out the "year zero" problem and other similar "gotchas" in my earlier sketch.

Generating weekly dates

I am sure this has been done before so i am looking for an efficient solution instead of own custom solution.
Given 2 dates, I am trying to generate the accurate weekly date (for creating weekly orders).
EDIT: I need to use .NET standard library to do this.
Example below,
Given 28/02/2012 and 6/03/2012.
so, the weekly dates generated are
- Week From(Start Monday): Week To(End Sunday):
- 27/02/2012 - 04/03/2012
- 05/03/2012 - 11/03/2012
Another example (1 month)
Given 01/02/2012 and 29/02/2012
so, the weekly dates generated are
- Week From(Start Monday): Week To(End Sunday):
- 30/01/2012 - 05/02/2012
- 06/02/2012 - 12/02/2012
- 13/02/2012 - 19/02/2012
- 20/02/2012 - 26/02/2012
- 27/02/2012 - 04/03/2012
I am doing this in c#. Has this been done before? Mind sharing the solutions?
Cheers
Here's a solution using Noda Time. Admittedly it requires a <= operator which I'm just implementing right now - but that shouldn't take long :)
using System;
using NodaTime;
class Test
{
static void Main()
{
ShowDates(new LocalDate(2012, 2, 28), new LocalDate(2012, 3, 6));
ShowDates(new LocalDate(2012, 2, 1), new LocalDate(2012, 2, 29));
}
static void ShowDates(LocalDate start, LocalDate end)
{
// Previous is always strict - increment start so that
// it *can* be the first day, then find the previous
// Monday
var current = start.PlusDays(1).Previous(IsoDayOfWeek.Monday);
while (current <= end)
{
Console.WriteLine("{0} - {1}", current,
current.Next(IsoDayOfWeek.Sunday));
current = current.PlusWeeks(1);
}
}
}
Obviously it's possible to do this in normal DateTime as well, but there's no real representation of "just a date" which makes the code less clear - and you'd need to implement Previous yourself.
EDIT: For example, in this case you might use:
using System;
class Test
{
static void Main()
{
ShowDates(new DateTime(2012, 2, 28), new DateTime(2012, 3, 6));
ShowDates(new DateTime(2012, 2, 1), new DateTime(2012, 2, 29));
}
static void ShowDates(DateTime start, DateTime end)
{
// In DateTime, 0=Sunday
var daysToSubtract = ((int) start.DayOfWeek + 6) % 7;
var current = start.AddDays(-daysToSubtract);
while (current <= end)
{
Console.WriteLine("{0} - {1}", current, current.AddDays(6));
current = current.AddDays(7);
}
}
}
Assuming you don't have to figure out that the start date is a monday:
var slots = new List<Tuple<DateTime, DateTime>>();
DateTime start = new DateTime(2012, 2, 28);
DateTime end = new DateTime(2012, 3, 6);
for (DateTime i = start; i < end; i = i.AddDays(7))
{
slots.Add(new Tuple<DateTime, DateTime>(i, i.AddDays(6)));
}
foreach (var slot in slots)
{
Console.WriteLine("{0}\t{1}", slot.Item1.ToString("dd/MM/yyyy"), slot.Item2.ToString("dd/MM/yyyy"));
}
Edit: Assuming you have to figure out what monday and what sunday covers the date range, you can move one day backwards till you hit a monday, and a day forward till you hit a sunday.
class Program
{
static void Main(string[] args)
{
var slots = new List<Tuple<DateTime, DateTime>>();
DateTime start = FirstMonday(new DateTime(2012, 2, 28));
DateTime end = FirstSunday(new DateTime(2012, 3, 6));
for (DateTime i = start; i < end; i = i.AddDays(7))
{
slots.Add(new Tuple<DateTime, DateTime>(i, i.AddDays(6)));
}
foreach (var slot in slots)
{
Console.WriteLine("{0}\t{1}", slot.Item1.ToString("dd/MM/yyyy"), slot.Item2.ToString("dd/MM/yyyy"));
}
Console.ReadLine();
}
static DateTime FirstMonday(DateTime date)
{
while (date.DayOfWeek != DayOfWeek.Monday) date = date.AddDays(-1);
return date;
}
static DateTime FirstSunday(DateTime date)
{
while (date.DayOfWeek != DayOfWeek.Sunday) date = date.AddDays(1);
return date;
}
}
This solution allows you to customize your start and end DayOfWeek:
Solution:
public Dictionary<DateTime, DateTime> GetWeeklyDateTimes(DateTime from, DateTime to, DayOfWeek startDay, DayOfWeek endDay)
{
int startEndSpan = 7 - endDay - startDay;
// Subtract days until it falls on our desired start day
from = from.AddDays(startDay - from.DayOfWeek);
// Add days until it falls on our desired end day
to = to.AddDays(to.DayOfWeek - endDay + 2);
Dictionary<DateTime, DateTime> dateTimes = new Dictionary<DateTime, DateTime>();
while (to.Subtract(from).Days > startEndSpan)
{
dateTimes.Add(from, from.AddDays(startEndSpan));
from = from.AddDays(startEndSpan + 1);
}
return dateTimes;
}
Example Usage:
// DateTime(2012, 2, 1) corresponds to Year 2012, Month February, Day 1
Dictionary<DateTime, DateTime> dateTimes = GetWeeklyDateTimes(new DateTime(2012, 2, 1), new DateTime(2012, 2, 29), DayOfWeek.Monday, DayOfWeek.Sunday);
foreach (KeyValuePair<DateTime, DateTime> entry in dateTimes)
{
Trace.WriteLine(entry.Key.ToString() + " " + entry.Value.ToString());
}

Categories

Resources