I'm not a super expert on LINQ, I've a data below provided by third party:
Data
Start: 6:00
End: 6:30
value: 1
Start: 7:00
End: 7:30
value: 1
Start: 8:00
End: 8:30
value: 1
Start: 9:00
End: 9:30
value: 1
Start: 10:00
End: 10:30
value: 1
Start: 11:00
End: 11:30
value: 1
Start: 12:00
End: 12:30
value: 1
Start: 13:00
End: 13:30
value: 1
Start: 14:00
End: 14:30
value: 1
...
Start: 05:00
End: 05:30
value: 1
This data keeps going for a week then 30 days and 365days.
I need to transform each 30minute block in to an hour.
e.g
Start: 6:00
End: 7:00
Value: 2
Start:7:00
End: 8:00
Value:2
......
Assuming that Start, End and Value comes as one row, could someone help how above can be achieved?
This query is able to group by the given AggregationType and it is able to filter out incomplete groups using the second parameter checkType.
private enum AggerationType { Year = 1, Month = 2, Day = 3, Hour = 4 }
private IList<Data> RunQuery(AggerationType groupType, AggerationType checkType)
{
// The actual query which does to trick
var result =
from d in testList
group d by new {
d.Start.Year,
Month = (int)groupType >= (int)AggerationType.Month ? d.Start.Month : 1,
Day = (int)groupType >= (int)AggerationType.Day ? d.Start.Day : 1,
Hour = (int)groupType >= (int)AggerationType.Hour ? d.Start.Hour : 1
} into g
// The where clause checks how much data needs to be in the group
where CheckAggregation(g.Count(), checkType)
select new Data() { Start = g.Min(m => m.Start), End = g.Max(m => m.End), Value = g.Sum(m => m.Value) };
return result.ToList();
}
private bool CheckAggregation(int groupCount, AggerationType checkType)
{
int requiredCount = 1;
switch(checkType)
{
// For year all data must be multiplied by 12 months
case AggerationType.Year:
requiredCount = requiredCount * 12;
goto case AggerationType.Month;
// For months all data must be multiplied by days in month
case AggerationType.Month:
// I use 30 but this depends on the given month and year
requiredCount = requiredCount * 30;
goto case AggerationType.Day;
// For days all data need to be multiplied by 24 hour
case AggerationType.Day:
requiredCount = requiredCount * 24;
goto case AggerationType.Hour;
// For hours all data need to be multiplied by 2 (because slots of 30 minutes)
case AggerationType.Hour:
requiredCount = requiredCount * 2;
break;
}
return groupCount == requiredCount;
}
Here some Test data if you want:
class Data
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int Value { get; set; }
}
// Just setup some test data simulary to your example
IList<Data> testList = new List<Data>();
DateTime date = DateTime.Parse("6:00");
// This loop fills just some data over several years, months and days
for (int year = date.Year; year > 2010; year--)
{
for(int month = date.Month; month > 0; month--)
{
for (int day = date.Day; day > 0; day--)
{
for(int hour = date.Hour; hour > 0; hour--)
{
DateTime testDate = date.AddHours(-hour).AddDays(-day).AddMonths(-month).AddYears(-(date.Year - year));
testList.Add(new Data() { Start = testDate, End = testDate.AddMinutes(30), Value = 1 });
testList.Add(new Data() { Start = testDate.AddMinutes(30), End = testDate.AddHours(1), Value = 1 });
}
}
}
}
Below is the code. It seems a little bit ugly because of switch statement. It would be better to refactor it but it should show the idea.
var items = input.Split('\n');
Func<string, string> f = s =>
{
var strings = s.Split(new[] {':'}, 2);
var key = strings[0];
var value = strings[1];
switch (key.ToLower())
{
case "start":
return s;
case "value":
return String.Format("{0}: {1}", key, Int32.Parse(value) + 1);
case "end":
return String.Format("{0}: {1:h:mm}", key,
DateTime.Parse(value) +
TimeSpan.FromMinutes(30));
default:
return "";
}
};
var resultItems = items.Select(f);
Console.Out.WriteLine("result = {0}",
String.Join(Environment.NewLine, resultItems));
It's actually quite hard to completely approach this with with pure LINQ. To make life easier, you'll need to write atleast one helper method that allows you to transform an enumeration. Take a look at the example below. Here I make use of an IEnumerable of TimeInterval and have a custom Split method (implemented with C# iterators) that Joins two elements together in one Tuple:
class TimeInterval
{
DateTime Start;
DateTime End;
int Value;
}
IEnumerable<TimeInterval> ToHourlyIntervals(
IEnunumerable<TimeInterval> halfHourlyIntervals)
{
return
from pair in Split(halfHourlyIntervals)
select new TimeInterval
{
Start = pair.Item1.Start,
End = pair.Item2.End,
Value = pair.Item1.Value + pair.Item2.Value
};
}
static IEnumerable<Tuple<T, T>> Split<T>(
IEnumerable<T> source)
{
using (var enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
T first = enumerator.Current;
if (enumerator.MoveNext())
{
T second = enumerator.Current;
yield return Tuple.Create(first, second);
}
}
}
}
The same can be applied to the first part of the problem (extracting half hourly TimeIntervals from the list of strings):
IEnumerable<TimeInterval> ToHalfHourlyIntervals(
IEnumerable<string> inputLines)
{
return
from triple in TripleSplit(inputLines)
select new TimeInterval
{
Start = DateTime.Parse(triple.Item1.Replace("Start: ", "")),
End = DateTime.Parse(triple.Item2.Replace("End: ", "")),
Value = Int32.Parse(triple.Item3)
};
}
Here I make use of a custom TripleSplit method that returns a Tuple<T, T, T> (which will be easy to write). With this in place, the complete solution would look like this:
// Read data lazilzy from disk (or any other source)
var lines = File.ReadLines(path);
var halfHourlyIntervals = ToHalfHourlyIntervals(lines);
var hourlyIntervals = ToHourlyIntervals(halfHourlyIntervals);
foreach (var interval in hourlyIntervals)
{
// process
}
What's nice about this solution is that it is completely deferred. It processes one line at a time, which allows you to process indefinately big sources without the danger of any out of memory exception, which seems important considering your given requirement:
This data keeps going for a week then 30 days and 365days.
Related
Writing a small application to calculate interest but the rate changes yearly. Needed to break the range into smaller date ranges when ever it crosses a year boundary. I wrote a little for loop to do it but it's rather clunky. Wondering if there are any built in functions to do this in C# (possible linq). Would basically be looking to return a list of date ranges with the corresponding base year (shortened code for readability).
static void Main(string[] args)
{
var dateStart = DateTime.Parse("2/10/2018");
var dateEnd = DateTime.Parse("3/10/2021");
var years = Years(dateStart, dateEnd);
var baseYear = dateStart.Year;
Console.WriteLine(baseYear);
var loopDateStart = dateStart;
var loopDateEnd = DateTime.Now;
for (int i = 0; i < years + 1; i++)
{
if (i < years) {
loopDateEnd = DateTime.Parse("1/1/" + (baseYear + 1));
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
loopDateStart = loopDateEnd;
baseYear++;
}
else {
loopDateEnd = dateEnd;
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
}
}
}
public static int Years(DateTime start, DateTime end)
{
return (end.Year - start.Year - 1) +
(((end.Month > start.Month) ||
((end.Month == start.Month) && (end.Day >= start.Day))) ? 1 : 0);
}
Sure, we can use LINQ:
var x = Enumerable.Range(dateStart.Year, (dateEnd.Year-dateStart.Year)+1)
.Select(y => new{
F = new[]{dateStart, new DateTime(y,1,1)}.Max(),
T = new[]{dateEnd, new DateTime(y,12,31)}.Min()
});
It generates an enumerable list of objects that have an F and a T property (from and to) that are your ranges.
It works by using Enumerable.Range to make a list of years: 2018,2019,2020,2021 by starting at 2108 and proceeding for 4 years (2018 to 2018 is one year entry, 2018 to 2021 is 4 year entries)
Then we just turn them into dates using new DateTime(year,amonth,aday) - when were making start dates, amonth and aday are 1 and 1, when making end dates they're 12 and 31
Then we just ask for every year y, "which date is greater, the startdate, or the 1-Jan-y" and "which date is lesser, the enddate or the 31-Dec-y " - for the initial and final date entry it's the startdate and the enddate that are greater and lesser. For other years it's the jan/dec dates. This gives the ranges you want
foreach(var xx in x){
Console.WriteLine(xx.F +" to "+xx.T);
}
2/10/2018 12:00:00 AM to 12/31/2018 12:00:00 AM
1/1/2019 12:00:00 AM to 12/31/2019 12:00:00 AM
1/1/2020 12:00:00 AM to 12/31/2020 12:00:00 AM
1/1/2021 12:00:00 AM to 3/10/2021 12:00:00 AM
If you want to do other work like the number of days between, you can do xx.T-xx.F in the loop, to make a timespan etc
Try:
var start = DateTime.Parse("4/5/2017");
var end = DateTime.Parse("3/1/2019");
DateTime chunkEnd;
for (var chunkStart = start; chunkStart < end; chunkStart = chunkEnd.AddDays(1))
{
var lastDay = new DateTime(chunkStart.Year, 12, 31);
chunkEnd = end > lastDay ? lastDay : end;
var days = (chunkEnd - chunkStart).Days;
Console.WriteLine($"{chunkStart:d} - {chunkEnd:d}; {days} days");
}
Produces:
4/5/2017 - 12/31/2017; 270 days
1/1/2018 - 12/31/2018; 364 days
1/1/2019 - 3/1/2019; 59 days
I came up with the following:
static IEnumerable<(DateTime,DateTime)> ChunkByYear(DateTime start, DateTime end)
{
// Splits <start,end> into chunks each belonging to a different year
while(start <= end)
{
var tempEnd = new DateTime(start.Year, 12, 31);
if(tempEnd >= end ) {
yield return (start, end);
yield break;
}
yield return (start, tempEnd);
start = tempEnd.AddDays(1);
}
}
Here are some results:
4/05/2017 to 3/01/2019:
4/05/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->3/01/2019
4/05/2017 to 4/05/2017:
4/05/2017->4/05/2017
31/12/2017 to 31/12/2019:
31/12/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->31/12/2019
31/12/2019 to 31/12/2019:
31/12/2019->31/12/2019
31/12/2018 to 1/01/2019:
31/12/2018->31/12/2018
1/01/2019->1/01/2019
Group by years:
using System.Collections.Generic;
using System.Linq;
...
static void Main(string[] args)
{
DateTime dateStart = DateTime.Parse("2/10/2018");
DateTime dateEnd = DateTime.Parse("3/10/2021");
// Group all possible dates by year
foreach(var group in GetDates(dateStart, dateEnd).GroupBy(date => date.Year))
{
Console.WriteLine(group.Key); // The key of the group is year
Console.WriteLine($"{group.Min()} ... {group.Max()}"); // Range: From minimum to maximum, order doesn't matter.
Console.WriteLine($"{group.First()} ... {group.Last()}"); //or Range version 2: From first to last, order matters.
Console.WriteLine(group.Count()); // Count days
}
}
/// <summary>
/// Get all days blindly, might need to pay attention to days on the boundaries
/// </summary>
private static IEnumerable<DateTime> GetDates(DateTime start, DateTime end)
{
// TODO: Check start <= end;
DateTime current = start;
while(current <= end)
{
yield return current;
current = current.AddDays(1);
}
}
I'm working on a scheduling algorithm that generates/assigns time-slots to a List of Recipients based on the following restrictions:
Max Recipients per Minute
Max Recipients per Hour
Suppose that the delivery Start Time is 2018-10-17 9:00 AM and we have 19 recipients with Max of 5 per min and and 10 per hour, so the output should be:
5 Recipients will be scheduled on 2018-10-17 9:00 AM
5 Recipients will be scheduled on 2018-10-17 9:01 AM
5 Recipients will be scheduled on 2018-10-17 10:00 AM
4 Recipients will be scheduled on 2018-10-17 10:01 AM
The algorithm is very accurate, but the way it works is as following:
First it generates a list of time-slots or time-windows that are accurately fits the no. of recipients based on the restrictions i mentioned before.
then, I'm moving whatever available in the List of Time-Slots for each set/group or recipients.
in the list of Time-Slots I added a counter that increments for every recipient added to it, so in this way I can track the no. of each recipients added to each time-slot to respect the Max per Min./Hr restrictions.
The previous process it simplified in this code snippet - I'm using While Loop to iterate, in my case when having 500K recipients this is taking 28 minutes to get it done!
I tried to use Parallel.ForEach but I couldn't figure out how to implement it in this case.
DateTime DeliveryStart = DateTime.Now;
//This list has DateTime: Time-windows values starting from DeliveryStart to the Max value of the time needed to schedule the Recipients
var listOfTimeSlots = new List<Tuple<DateTime, bool, int>>();
//List of Recipients with Two types of data: DateTime to tell when its scheduled and int value refers to the Recipient's ID
var ListOfRecipients = new List<Tuple<DateTime, int>>();
List<Tuple<int, DateTime>> RecipientsWithTimeSlots= new List<Tuple<int, DateTime>>();
int noOfRecipients = ListOfRecipients.Count;
int Prevhour = 0, _AddedPerHour = 0, Prevday = 0;
// Scheduling restrictions
int _MaxPerHour = 5400, _MaxPerMinute = 90;
int i = 0;
int indexStart = 0;
// ...
// ...
// Code to fill listOfTimeSlots ListOfRecipients with Data
while (noOfRecipients > 0)
{
var TimeStamp = listOfTimeSlots[i];
int hour = TimeStamp.Item1.Hour;
int day = TimeStamp.Item1.Day;
if (Prevhour == 0)
{
Prevhour = hour;
Prevday = day;
}
if (Prevhour != hour)
{
Prevhour = hour;
_AddedPerHour = 0;
}
if (_AddedPerHour >= _MaxPerHour)
{
var tmpItem = listOfTimeSlots.Where(l => l.Item1.Hour == hour && l.Item1.Day == day).LastOrDefault();
int indexOfNextItem = listOfTimeSlots.LastIndexOf(tmpItem) + 1;
i = indexOfNextItem;
_AddedPerHour = 0;
continue;
}
else
{
int endIndex;
endIndex = _MaxPerMinute > noOfRecipients ? noOfRecipients : _MaxPerMinute;
if (endIndex > Math.Abs(_AddedPerHour - _MaxPerHour))
endIndex = Math.Abs(_AddedPerHour - _MaxPerHour);
var RecipientsToIteratePerMinute = ListOfRecipients.GetRange(indexStart, endIndex);
foreach (var item in RecipientsToIteratePerMinute)
{
RecipientsWithTimeSlots.Add(new Tuple<int, DateTime>(item.Item2, TimeStamp.Item1));
listOfTimeSlots[i] = new Tuple<DateTime, bool, int>(TimeStamp.Item1, true, listOfTimeSlots[i].Item3 + 1);
_AddedPerHour++;
}
indexStart += endIndex;
noOfRecipients -= endIndex;
i++;
}
}
I simplified the code in here, for not making it so complex to understand, all i want it to speed-up the while loop or replacing it with a Parallel.ForEach.
THE WHILE LOOP IS NEVER SIMPLIFIED, THIS IS HOW IT EXACTLY WORKS \
Any help or suggestion is appreciated.
Here is a different approach. It creates the groups of ids first, then assigns them the date based on the requirements.
First, a class to represent the groups (avoid them tuples):
public class RecipientGroup
{
public RecipientGroup(DateTime scheduledDateTime, IEnumerable<int> recipients)
{
ScheduledDateTime= scheduledDateTime;
Recipients = recipients;
}
public DateTime ScheduledDateTime { get; private set; }
public IEnumerable<int> Recipients { get; private set; }
public override string ToString()
{
return string.Format($"Date: {ScheduledDateTime.ToShortDateString()} {ScheduledDateTime.ToLongTimeString()}, count: {Recipients.Count()}");
}
}
Then a class to iterate through the groups. You will see why this is needed later:
public class GroupIterator
{
public GroupIterator(DateTime scheduledDateTime)
{
ScheduledDateTime = scheduledDateTime;
}
public DateTime ScheduledDateTime { get; set; }
public int Count { get; set; }
}
Now, the code:
DateTime DeliveryStart = new DateTime(2018, 10, 17);
//List of Recipients (fake populate function)
IEnumerable<int> allRecipients = PopulateRecipients();
// Scheduling restrictions
int maxPerMinute = 90;
int maxPerHour = 270;
//Creates groups broken down by the max per minute.
var groupsPerMinute = allRecipients
.Select((s, i) => new { Value = s, Index = i })
.GroupBy(x => x.Index / maxPerMinute)
.Select(group => group.Select(x => x.Value).ToArray());
//This will be the resulting groups
var deliveryDateGroups = new List<RecipientGroup>();
//Perform an aggregate run on the groups using the iterator
groupsPerMinute.Aggregate(new GroupIterator(DeliveryStart), (iterator, ids) =>
{
var nextBreak = iterator.Count + ids.Count();
if (nextBreak >= maxPerHour)
{
//Will go over limit, split
var difference = nextBreak-maxPerHour;
var groupSize = ids.Count() - difference;
//This group completes the batch
var group = new RecipientGroup(iterator.ScheduledDateTime, ids.Take(groupSize));
deliveryDateGroups.Add(group);
var newDate = iterator.ScheduledDateTime.AddHours(1).AddMinutes(-iterator.ScheduledDateTime.Minute);
//Add new group with remaining recipients.
var stragglers = new RecipientGroup(newDate, ids.Skip(groupSize));
deliveryDateGroups.Add(stragglers);
return new GroupIterator(newDate, difference);
}
else
{
var group = new RecipientGroup(iterator.ScheduledDateTime, ids);
deliveryDateGroups.Add(group);
iterator.ScheduledDateTime = iterator.ScheduledDateTime.AddMinutes(1);
iterator.Count += ids.Count();
return iterator;
}
});
//Output minute group count
Console.WriteLine($"Group count: {deliveryDateGroups.Count}");
//Groups by hour
var byHour = deliveryDateGroups.GroupBy(g => new DateTime(g.ScheduledDateTime.Year, g.ScheduledDateTime.Month, g.ScheduledDateTime.Day, g.ScheduledDateTime.Hour, 0, 0));
Console.WriteLine($"Hour Group count: {byHour.Count()}");
foreach (var group in byHour)
{
Console.WriteLine($"Date: {group.Key.ToShortDateString()} {group.Key.ToShortTimeString()}; Count: {group.Count()}; Recipients: {group.Sum(g => g.Recipients.Count())}");
}
Output:
Group count: 5556
Hour Group count: 1852
Date: 10/17/2018 12:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 1:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 2:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 3:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 4:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 5:00 AM; Count: 3; Recipients: 270
... and so on for all 1852 groups.
This takes about 3 seconds to complete.
I am sure there are edge cases. I wrote this in a hurry so just think about those.
I have string "Mon-Thu, Sun".
I need to convert it into new List<DayOfWeek>{DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Sunday}
I think about spliting this string into string array and then parse strings into DateTime with dateformat "ddd". But I need to detect somehow where is "-" symbol and where is ",".
But next code fails
var str = "Mon-Thu, Sun";
var split = str.Split(new []{',', '-'});
foreach(var item in split){
Console.WriteLine(item.Trim());
var day = DateTime.ParseExact(item.Trim(), "ddd", CultureInfo.InvariantCulture);
Console.WriteLine(day.ToShortDateString());
}
With error "String was not recognized as a valid DateTime because the day of week was incorrect."
It turns out that C# libraries do indeed maintain a list of day abbreviations, and if you don't like them, you can even change them. Specifically, I'm referring to CultureInfo.[culture].DateTimeFormat.AbbreviatedDayNames.
The InvariantCulture uses the same abbreviations for Monday, Thursday, and Sunday as you've listed in your question.
Given an abbreviation for a day name, you should be able to derive the index of the abbreviated name in the AbbreviatedDayNames array, which matches the index used by DayOfWeek.
To me, this approach seems superior than embedding literal strings into your code.
public static void Main()
{
var dayList = new List<DayOfWeek>();
var str = "Mon-Thu, Sun";
str = str.Replace(" ", string.Empty); // remove spaces
// split groups by comma
var split = str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var item in split) // process each group
{
// split ranges by hyphen
var elements = item.Split(new[] {'-'}, StringSplitOptions.RemoveEmptyEntries); // split group into elements
switch (elements.Length)
{
case 1:
// add single element
dayList.Add((DayOfWeek) GetDayIndex(elements[0]));
break;
case 2:
// add range of elements
dayList.AddRange(GetDayRange(elements[0], elements[1]));
break;
default:
Console.WriteLine($"Input line does not match required format: \"{str}\"");
break;
}
}
// prove it works
Console.WriteLine(string.Join(", ", dayList));
}
private static int GetDayIndex(string dayNameAbbreviation)
{
return Array.IndexOf(CultureInfo.InvariantCulture.DateTimeFormat.AbbreviatedDayNames, dayNameAbbreviation);
}
private static IEnumerable<DayOfWeek> GetDayRange(string beginDayNameAbbrev, string endDayNameAbbrev)
{
var dayRange = new List<DayOfWeek>();
for (var i = GetDayIndex(beginDayNameAbbrev); i <= GetDayIndex(endDayNameAbbrev); i++)
{
dayRange.Add((DayOfWeek) i);
}
return dayRange;
}
EDIT
As stated above, if you don't like the day abbreviations used by a particular culture, you can temporarily change them. To see how, have a look at this Stack Overflow question: How to change DateTimeFormatInfo.CurrentInfo AbbreviatedDayNames collection.
One way to do this would be to first split the string into "chunks", which I'm defining as a range of one or more days, which are separated by the comma character. Then, for each chunk, grab the start day, add it to the list, and then increment it until we get to the end day.
We can write the code to the increment the days such that they will "wrap around" the week. For example, if we were representing some vacation time that we were going to take from "Fri-Mon", the days would be Friday, Saturday, Sunday, and Monday. Just incrementing alone would end up with an invalid value, since Sunday is 0.
We can use Enum.GetValues combined with the System.Linq Cast method to get the string values of the days of week, and then just compare to find which day of the week starts with our input.
static void Main(string[] args)
{
var input = "Fri-Thu, Sun";
var consecutiveChunks = input.Split(new[] { ',' },
StringSplitOptions.RemoveEmptyEntries);
var output = new List<DayOfWeek>();
var daysOfWeek = Enum.GetValues(typeof(DayOfWeek)).Cast<DayOfWeek>();
foreach (var chunk in consecutiveChunks)
{
var chunkRange = chunk.Split('-').Select(i => i.Trim()).ToList();
DayOfWeek currentDay = daysOfWeek
.First(d => d.ToString().StartsWith(chunkRange[0]));
DayOfWeek lastDay = chunkRange.Count > 1
? daysOfWeek.First(d => d.ToString().StartsWith(chunkRange[1]))
: currentDay;
output.Add(currentDay);
// If we have a range of days, add the rest of them
while (currentDay != lastDay)
{
// Increment to the next day
if (currentDay == DayOfWeek.Saturday)
{
currentDay = DayOfWeek.Sunday;
}
else
{
currentDay++;
}
output.Add(currentDay);
}
}
// Output our results:
Console.WriteLine($"The ranges, \"{input}\" resolve to:");
output.ForEach(i => Console.WriteLine(i.ToString()));
Console.Write("\nDone!\nPress any key to exit...");
Console.ReadKey();
}
Output
The following code works for the format you mentioned.
Input : "Mon-Thu, Sun"
OutPut: Monday, Tuesday, Wednesday, Thursday, Sunday
Input : "Mon, Wed-Thu, Sun"
OutPut: Monday, Wednesday, Thursday, Sunday
List<DayOfWeek> ListOfDays()
{
var str = "Mon-Thu, Sun";
string[] split = str.Split(',');
var days = new List<DayOfWeek>();
foreach (var item in split)
{
if (item.IndexOf('-') < 0)
{
days.Add(GetDayOfWeek(item.Trim()));
continue;
}
var consecutiveDays = item.Split('-');
DayOfWeek startDay = GetDayOfWeek(consecutiveDays[0].Trim());
DayOfWeek endDay = GetDayOfWeek(consecutiveDays[1].Trim());
for (DayOfWeek day = startDay; day <= endDay; day++)
days.Add(day);
}
return days;
}
DayOfWeek GetDayOfWeek(string day)
{
switch (day.ToUpper())
{
case "MON":
return DayOfWeek.Monday;
break;
case "TUE":
return DayOfWeek.Tuesday;
break;
case "WED":
return DayOfWeek.Wednesday;
break;
case "THU":
return DayOfWeek.Thursday;
break;
case "FRI":
return DayOfWeek.Friday;
break;
case "SAT":
return DayOfWeek.Saturday;
break;
case "SUN":
return DayOfWeek.Sunday;
break;
default:
throw new ArgumentException("Invalid day");
break;
}
}
This is because when you specify only day of week when parsing, it defaults to DateTime.Now as in the day you are running the program. So if you pass a day different than what it is today you get an error. You would have to parse it yourself e.g. by doing
Dictionary<string, DayOfWeek> days = new Dictionary<string, DayOfWeek>
{
["Mon"] = DayOfWeek.Monday,
["Tue"] = DayOfWeek.Tuesday,
["Wed"] = DayOfWeek.Wednesday,
["Thu"] = DayOfWeek.Thursday,
["Fri"] = DayOfWeek.Friday,
["Sat"] = DayOfWeek.Saturday,
["Sun"] = DayOfWeek.Sunday
};
//Get the next day in the week by calculating modulo 7
DayOfWeek NextDay(DayOfWeek day) => (DayOfWeek)(((int)day + 1) % 7);
List<DayOfWeek> GetDays(string input)
{
var ranges = input.Split(',');
var daysList = new List<DayOfWeek>();
foreach(var range in ranges)
{
var bounds = range.Split('-').Select(s => s.Trim()).ToList();
if(bounds.Count == 1)
{
if(days.TryGetValue(bounds[0], out var day))
daysList.Add(day);
else
throw new FormatException("Couldn't find day");
}
else if(bounds.Count == 2)
{
if(days.TryGetValue(bounds[0], out var begin) && days.TryGetValue(bounds[1], out var end))
{
if(begin == NextDay(end)) // whole week in one range
{
daysList.AddRange(days.Values);
break;
}
for(var i = begin; i != NextDay(end); i = NextDay(i))
{
daysList.Add(i);
}
}
else
throw new FormatException("Couldn't find day");
}
else
throw new FormatException("Too many hyphens in one range");
}
var set = new SortedSet<DayOfWeek>(daysList); //remove duplicates and sort
return set.ToList();
}
var input = "Mon-Thu, Sun";
foreach(var day in GetDays(input))
{
Console.WriteLine(day);
}
EDIT: added answer :)
Mon is not standard day input for C#. First you will have to convert it to correct equivalent day value in DayOfWeek enum manually based on what all formats you would like to support. Like Mon should be Monday etc. Once you have correct equivalent, you can easily map it to the DayOfWeek Enum.
I have an initial and a final date range = 1/1/2015 - 1/30/2015
I have these date ranges that represent dates of unavailability.
1/5/2015 - 1/10/2015
1/15/2015 - 1/20/2015
1/22/2015 - 1/28/2015
I want this output, mainly the dates of availability from the main range:
A: 1/1/2015 - 1/4/2015
B: 1/11/2015 - 1/14/2015
C: 1/21/2015 - 1/21/2015
D: 1/29/2015 - 1/30/2015
I tried to generate a sequential date range like this in order to get the exception dates with Except() but I think I'm complicating the thing.
//dtStartDate = 1/1/2015
//dtEndDate = 1/30/2015
var days = (int)(dtEndDate - dtStartDate).TotalDays + 1;
var completeSeq = Enumerable.Range(0, days).Select(x => dtStartDate.AddDays(x)).ToArray();
How can I get the gap of date ranges from period of time.
I other words how can I get the A, B, C and D from this picture
http://www.tiikoni.com/tis/view/?id=ebe851c
If these dates overlap, they must not be considered only where is a gap.
----------UPDATE-----------
I think if I do this:
var range = Enumerable.Range(0, (int)(1/10/2015 - 1/5/2015).TotalDays + 1).Select(i => 1/5/2015.AddDays(i));
var missing = completeSeq.Except(range).ToArray();
for each date range I will have the exclusion of each date range given but still cannot get the gap!
I saw your question in my morning today and really liked it, but was busy the whole day. So, got a chance to play with your question and believe me I enjoyed it. Here is my code:-
DateTime startDate = new DateTime(2015, 1, 1);
DateTime endDate = new DateTime(2015, 1, 30);
int totalDays = (int)(endDate - startDate).TotalDays + 1;
availability.Add(new Availability { StartDate = endDate, EndDate = endDate });
var result = from x in Enumerable.Range(0, totalDays)
let d = startDate.AddDays(x)
from a in availability.Select((v, i) => new { Value = v, Index = i })
where (a.Index == availability.Count - 1 ?
d <= a.Value.StartDate : d < a.Value.StartDate)
&& (a.Index != 0 ? d > availability[a.Index - 1].EndDate : true)
group new { d, a } by a.Value.StartDate into g
select new
{
AvailableDates = String.Format("{0} - {1}",g.Min(x => x.d),
g.Max(x => x.d))
};
This, definitely need explanation so here it is:-
Step 1: Create a range of dates from Jan 01 till Jan 30 using Enumerable.Range
Step 2: Since after the second unavailable date range, we need to limit the dates selected from last endate till current object startdate, I have calculated index so that we can get access to the last enddate.
Step 3: Once we get the index, all we need to do is filter the dates except for first date range since we didn't have last object in this case.
Step 4: For the last item since we don't have the max range I am adding the endDate to our unavailable list (hope this makes sense).
Here is the Working Fiddle, if you get confused just remove group by and other filters and debug and see the resulting output it will look fairly easy :)
using System;
using System.Collections.Generic;
using System.Linq;
public static class Program {
public static void Main() {
Tuple<DateTime,DateTime> range=Tuple.Create(new DateTime(2015,1,1),new DateTime(2015,1,30));
Tuple<DateTime,DateTime>[] exclude=new[] {
Tuple.Create(new DateTime(2015,1,5),new DateTime(2015,1,10)),
Tuple.Create(new DateTime(2015,1,15),new DateTime(2015,1,20)),
Tuple.Create(new DateTime(2015,1,22),new DateTime(2015,1,28))
};
foreach(Tuple<DateTime,DateTime> r in ExcludeIntervals(range,exclude)) {
Console.WriteLine("{0} - {1}",r.Item1,r.Item2);
}
}
public static IEnumerable<Tuple<DateTime,DateTime>> ExcludeIntervals(Tuple<DateTime,DateTime> range,IEnumerable<Tuple<DateTime,DateTime>> exclude) {
IEnumerable<Tuple<DateTime,bool>> dates=
new[] { Tuple.Create(range.Item1.AddDays(-1),true),Tuple.Create(range.Item2.AddDays(1),false) }.
Concat(exclude.SelectMany(r => new[] { Tuple.Create(r.Item1,false),Tuple.Create(r.Item2,true) })).
OrderBy(d => d.Item1).ThenBy(d => d.Item2); //Get ordered list of time points where availability can change.
DateTime firstFreeDate=default(DateTime);
int count=1; //Count of unavailability intervals what is currently active. Start from 1 to threat as unavailable before range starts.
foreach(Tuple<DateTime,bool> date in dates) {
if(date.Item2) { //false - start of unavailability interval. true - end of unavailability interval.
if(--count==0) { //Become available.
firstFreeDate=date.Item1.AddDays(1);
}
} else {
if(++count==1) { //Become unavailable.
DateTime lastFreeDate=date.Item1.AddDays(-1);
if(lastFreeDate>=firstFreeDate) { //If next unavailability starts right after previous ended, then no gap.
yield return Tuple.Create(firstFreeDate,lastFreeDate);
}
}
}
}
}
}
ideone.com
Got a little oopy...
public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public bool HasStart
{
get { return Start != DateTime.MinValue; }
}
public bool IsInRange(DateTime date)
{
return (date >= this.Start && date <= this.End);
}
public List<DateRange> GetAvailableDates(DateRange excludedRange)
{
return GetAvailableDates(new List<DateRange>(){excludedRange});
}
public List<DateRange> GetAvailableDates(List<DateRange> excludedRanges)
{
if (excludedRanges == null)
{
return new List<DateRange>() { this };
}
var list = new List<DateRange>();
var aRange = new DateRange();
var date = this.Start;
while (date <= this.End)
{
bool isInARange = excludedRanges.Any(er => er.HasStart && er.IsInRange(date));
if (!isInARange)
{
if (!aRange.HasStart)
{
aRange.Start = date;
}
aRange.End = date;
}
else
{
if (aRange.HasStart)
{
list.Add(aRange);
aRange = new DateRange();
}
}
date = date.AddDays(1);
}
if (aRange.HasStart)
{
list.Add(aRange);
}
return list;
}
}
I have a list of dates that are apart by a month in the sense that all dates are the "First Monday of the month". In some cases months are missing so I need to write a function to determine if all dates are consecutive
So for example if this was the list of dates, the function would return true as all items are the "First Friday of the month" and there are no gaps. This example below would return true.
var date = new DateTime(2013, 1, 4);
var date1 = new DateTime(2013, 2, 1);
var date2 = new DateTime(2013, 3, 1);
var date3 = new DateTime(2013, 4, 5);
var dateArray = new DateTime[]{date, date1, date2, date3};
bool isConsecutive = IsThisListConsecutive(dateArray);
where this example below would return false because, even though they are also all "First Friday of the month", its missing the March 2013 item.
var date = new DateTime(2013, 1, 4);
var date1 = new DateTime(2013, 2, 1);
var date3 = new DateTime(2013, 4, 5);
var dateArray = new DateTime[]{date, date1, date3};
bool isConsecutive = IsThisListConsecutive(dateArray);
so i am trying to figure out the right logic for the IsThisListConsecutive() method:
Here was my first try: (Note I already know upfront that all dates are same day of week and same week of month so the only thing i am looking for is a missing slot)
private bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
{
DateTime firstDate = orderedSlots.First();
int count = 0;
foreach (var slot in orderedSlots)
{
if (slot.Month != firstDate.AddMonths(count).Month)
{
return false;
}
count++;
}
return true;
}
This code above works exept if the list crosses over from one year to another. I wanted to get any advice on a better way to create this function and how that line could be rewritten to deal with dates that cross over years.
So to implement this we'll start with a simple helper method that takes a sequence and returns a sequence of pairs that make up each item with it's previous item.
public static IEnumerable<Tuple<T, T>> Pair<T>(this IEnumerable<T> source)
{
T previous;
using (var iterator = source.GetEnumerator())
{
if (iterator.MoveNext())
previous = iterator.Current;
else
yield break;
while(iterator.MoveNext())
{
yield return Tuple.Create(previous, iterator.Current);
previous = iterator.Current;
}
}
}
We'll also use this simple method to determine if two dates are in the same month:
public static bool AreSameMonth(DateTime first, DateTime second)
{
return first.Year == second.Year
&& first.Month == second.Month;
}
Using that, we can easily grab the month of each date and see if it's the month after the previous month. If it's true for all of the pairs, then we have consecutive months.
private static bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
{
return orderedSlots.Pair()
.All(pair => AreSameMonth(pair.Item1.AddMonths(1), pair.Item2));
}
Note: This is completely untested, and the date checks are probably pretty bad or somewhat redundant, but that’s the best I could come up with right now ^^
public bool AreSameWeekdayEveryMonth(IEnumerable<DateTime> dates)
{
var en = dates.GetEnumerator();
if (en.MoveNext())
{
DayOfWeek weekday = en.Current.DayOfWeek;
DateTime previous = en.Current;
while (en.MoveNext())
{
DateTime d = en.Current;
if (d.DayOfWeek != weekday || d.Day > 7)
return false;
if (d.Month != previous.Month && ((d - previous).Days == 28 || (d - previous).Days == 35))
return false;
previous = d;
}
}
return true;
}
I would recommend looking at the TimeSpan structure. Thanks to operator overload you can get a TimeSpan by substracting two dates and then receive a TimeSpan that expresses the difference between the two dates.
http://msdn.microsoft.com/en-us/library/system.timespan.aspx
okay, your code doesnt work when the years cross over becuase jan 1st may be a monday on one year and a tuesday on the next. If I was doing this, I would first check that
a) they are the same day of the week in each month (use DateTime.DayOfWeek)
b) they are the same week of the month in each month*
use extension method DayOfMonth (see link)
* Calculate week of month in .NET *
(you said you already know a & b to be true so lets go on to the third condition)
c) we have to determine if they are in consecutive months
//order the list of dates & place it into an array for ease of looping
DateTime[] orderedSlots = slots.OrderBy( t => t).ToArray<DateTime>();
//create a variable to hold the date from the previous month
DateTime temp = orderedSlots[0];
for(i= 1; index < orderedSlots.Length; index++)
{
if((orderedSlots[index].Month != temp.AddMonths(1).Month |
orderedSlots[index].Year != temp.AddMonths(1).Year)){
return false;
}
previousDate = orderedSlots[index];
}
return true;
if you need to check conditions a & b as well add change the if statement as follows
if( orderedSlots[index].Month != temp.AddMonths(1).Month |
orderedSlots[index].Year != temp.AddMonths(1).Year) |
orderedSlots[index].DayOfWeek != temp.DayOfWeek |
orderedSlots[index].GetWeekOfMonth != temp.AddMonths(1).GetWeekOfMonth){
return false;
}
remember that to use the get week of month extension method you have to include the code in
Calculate week of month in .NET
I'm sure there are typos as I did this in a text editor.
Well, here is my initial thought on how I would approach this problem.
First, is to define a function that will turn the dates into the ordinal values corresponding to the order in which they should appear.
int ToOrdinal(DateTime d, DateTime baseline) {
if (d.Day <= 7
&& d.DayInWeek == baseline.DayInWeek) {
// Since there is only one "First Friday" a month, and there are
// 12 months in year we can easily compose the ordinal.
// (As per default.kramer's comment, months normalized to [0,11].)
return d.Year * 12 + (d.Month - 1);
} else {
// Was not correct "kind" of day -
// Maybe baseline is Tuesday, but d represents Wednesday or
// maybe d wasn't in the first week ..
return 0;
}
}
var dates = ..;
var baseline = dates.FirstOrDefault();
var ordinals = dates.Select(d => ToOrdinal(d, baseline));
Then, for the dates provided, we end up with ordinal sequences like:
[24156 + 0, 24156 + 1, 24156 + 2, 24156 + 3]
And
[24156 + 0, 24156 + 1, /* !!!! */ 24156 + 3]
From here it is just a trivial matter of iterating the list and ensuring that the integers occur in sequence without gaps or stalls - that is, each item/integer is exactly one more than the previous.
I could be misinterpreting what you are trying to do, but I think this will work, assuming you don't have to handle ancient dates. See if there are any gaps in the dates converted to "total months"
int totalMonths = date.Year * 12 + (date.Month - 1);