Related
I have below class
public class DateGenerator
{
public DateOnly GeneratedDate {get; set;}
}
and list of dateGenerators List<DateGenerator> dateGenerators; as below
I need to verify whether all months are present for a year within the given range. For example, I have given a listed range; I need to identify the years first, and then for that year, whether all months are listed or not.
Here I have the same years along with the months listed, so the above condition also applies for the same years.
For example, not all months are listed below for the year 2009 and I have duplicate data for 2011. All months are not listed in the second set of the same data year, 2011, so I need to identify this.
Date (Month/Day/Year)
7/1/2009
8/1/2009
9/1/2009
10/1/2009
11/1/2009
12/1/2009
1/1/2010
2/1/2010
3/1/2010
4/1/2010
5/1/2010
6/1/2010
7/1/2010
8/1/2010
9/1/2010
10/1/2010
11/1/2010
12/1/2010
1/1/2011
2/1/2011
3/1/2011
4/1/2011
5/1/2011
6/1/2011
7/1/2011
8/1/2011
9/1/2011
10/1/2011
11/1/2011
12/1/2011
4/1/2011
5/1/2011
6/1/2011
7/1/2011
8/1/2011
9/1/2011
I can group by GeneratedDate and extract the year and compare, but I am unsure how to tackle the duplicate data. Could anyone please let me know how to approach the same to identify
Thanks in advance!!
Sample method:
public bool verifyCompleteMonthlySet(List<DateGenerator> dateGenerators)
{
//Identify whether all months are present for a year in a
//given a list, and also identify if there is
//any duplicate set found for a year, and all months are present for that same year as well
}
With LINQ you can write:
bool allMonthsAreListed = dateGenerators
.GroupBy(dg => dg.GeneratedDate.Year)
.All(g => g.Count() % 12 == 0);
Explanation:
We group by year.
For each group get the month count.
If the month count is divisible by 12 (with no remainder), all the sets in this year are complete.
Note that this is not perfect, e.g. there could be two incomplete sets in the same year whose month count adds up to 12. We would need to have a way to identify the sets. Do you have a set id or the like?
Also, I am not sure what you mean by "I need to identify this".
Do only need to know that some years or sets are not complete?
You you need to identify the years/sets?
Do you need to know the missing months?
Since we do not have a way to identify sets, things get a bit more complicated:
bool allMonthsAreListed = dateGenerators
.Select(dg => dg.GeneratedDate)
.GroupBy(d => d.Year)
.All(
g => g.Count() % 12 == 0 &&
g.GroupBy(d => d.Month)
.Select(g2 => g2.Count())
.Distinct()
.Count() == 1
);
Explanation:
We extract the date from the DateGenerators.
We group by year.
We test whether the count of months per year is divisible by 12 and ...
... that all months occur the same number of times. To do this, we:
group by month
get the count of each month group
get the distinct number of counts for all the groups
test if this number is 1.
Note that if we have one full set, each month occurs once, if we have two full sets, each month occurs twice, etc. If the first set is complete and the second set has only 11 months, the we have 11 times a count of 2 and one time a count of 1. I.e., the distinct number of counts is 2. This identifies an incomplete set.
Tested with
List<DateGenerator> dateGenerators = new();
// Year 2010 with one complete set
for (int i = 1; i <= 12; i++) {
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2010, i, 1) });
}
// Year 2011 with 12 months but from two incomplete sets
for (int i = 1; i <= 10; i++) { // Set 1: months 1 to 10
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2011, i, 1) });
}
for (int i = 5; i <= 6; i++) { // Set 2: months 5 and 6
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2011, i, 1) });
}
Also using System.Linq.GroupBy but then doing a forensic by removing from all.
static void Main(string[] args)
{
Console.Title = "Missing months";
// Get an array of DateTime.
var dates = inputData.Trim().Split(',').Select(_=>DateTime.Parse(_)).ToArray();
foreach (var year in dates.GroupBy(_=>_.Year))
{
Console.WriteLine($"{year.Key}");
var months = year.Select(_ => _.Month).Distinct().ToArray();
if (months.Length == 12)
{
Console.WriteLine($"All months present.");
}
else
{
var all = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
foreach (var month in months)
{
all.Remove(month);
}
Console.WriteLine($"{year.Key} missing {string.Join(", ", all )}");
}
foreach (var dup in year.GroupBy(_ => _).Where(_ => _.Count() > 1))
{
Console.WriteLine($"Duplicate dates: {dup.Key.ToShortDateString()} ({dup.Count()})");
}
Console.WriteLine();
}
Console.ReadKey();
}
Where inputData uses the data set from your post:
const string inputData = #"
7/1/2009,
8/1/2009,
9/1/2009,
10/1/2009,
11/1/2009,
12/1/2009,
1/1/2010,
2/1/2010,
3/1/2010,
4/1/2010,
5/1/2010,
6/1/2010,
7/1/2010,
8/1/2010,
9/1/2010,
10/1/2010,
11/1/2010,
12/1/2010,
1/1/2011,
2/1/2011,
3/1/2011,
4/1/2011,
5/1/2011,
6/1/2011,
7/1/2011,
8/1/2011,
9/1/2011,
10/1/2011,
11/1/2011,
12/1/2011,
4/1/2011,
5/1/2011,
6/1/2011,
7/1/2011,
8/1/2011,
9/1/2011";
I have a list of times:
1:00
3:00
5:00
I need to select the most recent one compared to my current time.
So say the current time is 2:50, I need the code to select 1:00.
If the current time was 3:30, it would select 3:00, etc.
How can I write a for loop to accomplish this with any given list of times and a current time?
You can query times with a help of Linq MinBy, e.g. having
TimeOnly[] times = {
new TimeOnly(1, 0),
new TimeOnly(3, 0),
new TimeOnly(5, 0),
};
and
TimeOnly current = new TimeOnly(2, 50);
you can put
using System.Linq;
...
var mostRecent = times
.Where(time => time <= current) // past time only
.MinBy(time => current - time);
No Linq solution can be a for loop:
TimeOnly mostRecent = TimeOnly.MinValue;
foreach (TimeOnly time in times)
if (time <= current && mostRecent < time)
mostRecent = time;
Edit: If collection (TimeOnly[] times) is in fact ordered we can use binary search instead of scan (see Eric J. comment):
// if times is List<> then the syntax should be
// int index = times.BinarySearch(current);
int index = Array.BinarySearch(times, current);
TimeOnly mostRecent = index >= 0 ? times[index] : times[~index - 1];
Lets say I have collection of n workers. Lets say there are 3:
John
Adam
Mark
I want to know when they have to clean the office. If I set int cleanDays = 3 it would be something like that:
//Day of month;worker
1;John
2;John
3;John
4;Adam
5;Adam
6;Adam
7;Mark
8;Mark
9;Mark
10;John
11;John
.
.
.
If I set cleanDays = 1 it would be:
1;John
2;Adam
3;Mark
4;John
5;Adam
.
.
.
And so on.
I already managed something like this:
int cleanDays = 6;
for (int day=1; day<30;day++) { //for each day
int worker = (day-1 % cleanDays)%workers.Count; //get current worker (starting from index 0)
for (int times=0; times< cleanDays; times++) //worker do the job `cleanDays` times
Console.WriteLine(day++ + ";" +workers[worker].Name);
}
This is not working properly, because it gaves me 34 days. That because of day++ in first loop. But if I delete day++ from first loop:
for (int day=1; day<30;) { //for each day
int worker = (day-1 % cleanDays)%workers.Count; //get current worker (starting from index 0)
for (int times=0; times< cleanDays; times++) //worker do the job `cleanDays` times
Console.WriteLine(day++ + ";" +workers[worker].Name);
}
It is giving output only with first worker. When I debugged I saw that:
int worker = (day-1 % cleanDays)%workers.Count;
and worker was equal to 0 everytime. That means:
(20-1%6)%3 was equal to 0. Why does that happen?
UPDATE: I just read your question more carefully and realized you were not asking about the actual code at all. Your real question was:
That means: (20-1%6)%3 was equal to 0. Why does that happen?
First of all, it doesn't. (20-1%6)%3 is 1. But the logic is still wrong because you have the parentheses in the wrong place. You meant to write
int worker = (day - 1) % cleanDays % workers.Count;
Remember, multiplication, division and remainder operators are all higher precedence than subtraction. a + b * c is a + (b * c), not (a + b) * c. The same is true of - and %. a - b % c is a - (b % c), not (a - b) % c.
But I still stand by my original answer: you can eliminate the problem entirely by writing a query that represents your sequence operations, rather than a loop with a bunch of complicated arithmetic that is easy to get wrong.
Original answer follows.
Dmitry Bychenko's solution is pretty good but we can improve on it; modular arithmetic is not necessary here. Rather than indexing into the worker array, we can simply select-many from it directly:
var query = Enumerable.Repeat(
workers.SelectMany(worker => Enumerable.Repeat(worker, cleanDays)),
1000)
.SelectMany(workerseq => workerseq)
.Select((worker, index) => new { Worker = worker, Day = index + 1})
.Take(30);
foreach(var x in query)
Console.WriteLine($"Day {x.Day} Worker {x.Worker}");
Make sure you understand how this query works, because these are core operations of LINQ. We take a sequence of workers,
{A, B, C}
This is projected onto a sequence of sequences:
{ {A, A}, {B, B}, {C, C} }
Which is flattened:
{A, A, B, B, C, C}
We then repeat that a thousand times:
{ { A, A, B, B, C, C },
{ A, A, B, B, C, C },
...
}
And then flatten that sequence-of-sequences:
{ A, A, B, B, C, C, A, A, B, B, C, C, ... }
We then select-with-index into that flattened sequence to produce a sequence of day, worker pairs.
{ {A, 1}, {A, 2}, {B, 3}, {B, 4}, ... }
Then take the first 30 of those. Then we execute the query and print the results.
Now, you might say isn't this inefficient? If we have, say, 4 workers, we put each on 5 days, and then we repeat that sequence 1000 times; that makes a sequence with 5 x 4 x 1000 = 20000 items, but we only need the first 30.
Do you see what is wrong with that logic?
LINQ sequences are constructed lazily. Because of the Take(30) we never construct more than 30 pairs in the first place. We could have repeated it a million times; doesn't matter. You say Take(30) and the sequence construction will stop constructing more items after you've printed 30 of them.
But don't stop there. Ask yourself how you can improve this code further.
The bit with the days as integers seems a bit dodgy. Surely what you want is actual dates.
var start = new DateTime(2017, 1, 1);
And now instead of selecting out numbers, we can select out dates:
...
.Select((worker, index) => new { Worker = worker, Day = start.AddDays(index)})
...
What are the key takeaways here?
Rather than messing around with loops and weird arithmetic, just construct queries that represent the shape of what you want. What do you want? Repeat each worker n times. Great, then there should be a line in your program somewhere that says Repeat(worker, n), and now your program looks like its specification. Now your program is more likely to be correct. And so on.
Use the right data type for the job. Want to represent dates? Use DateTime, not int.
I would use a while loop, and use some tracking variables to keep track of which worker you are at and how many clean-times are left for that worker. Something like this:
const int cleanTime = 3; // or 1 or 6
var workers = new [] { "John", "Adam" , "Mark" }
var day = 1;
var currentWorker = 0;
var currentCleanTimeLeft = cleanTime;
while (day <= 30) {
Console.WriteLine("{0};{1}", day, workers[currentWorker].Name);
currentCleanTimeLeft--;
if (currentCleanTimeLeft == 0) {
currentCleanTimeLeft = cleanTime;
currentWorker++;
if (currentWorker >= workers.Length)
currentWorker = 0;
}
day++;
}
A very basic solution, no division or arithmatics required.
The second loop is unnecessary, it simply messes up your day.
int cleanDays = 6;
for (int day = 1; day <= 30; day++)
{
int worker = ((day-1) / cleanDays) % workers.Count;
Console.WriteLine(day + ";" + workers[worker].Name);
}
Example on Fiddle
The basic idea is to give each individual day an numerical value - DateTime.Now.DayOfYear is a good choice, or just a running count - and map that numerical value to an index in the Worker array.
The main logic is in the workerIndex line below:
It takes the day number and divides it by cleanDays. This means that each x days is mapped to the same workerIndex.
It takes the workerIndex and does a modulo operation on it (%) on the count of workers. This causes the workerIndex to by cyclical, iterating endlessly over all workers.
string[] workers = new string[] {"Mike", "Bob", "Hank"};
int cleanDays = 6;
for (int dayNum = 0 ; dayNum < 300 ; dayNum++)
{
var workerIndex = (dayNum / cleanDays) % workers.Length; // <-- LOGIC!
Console.WriteLine("Day {0} - Cleaner: {1}", dayNum, workers[workerIndex]);
}
I suggest modulo arithmetics and Linq:
List<Worker> personnel = ...
int days = 30;
int cleanDays = 4;
var result = Enumerable.Range(0, int.MaxValue)
.SelectMany(index => Enumerable
.Repeat(personnel[index % personnel.Count], cleanDays))
.Select((man, index) => $"{index + 1};{man.Name}")
.Take(days);
Test:
Console.Write(string.Join(environment.NewLine, result));
Output:
1;John
2;John
3;John
4;John
5;Adam
6;Adam
7;Adam
8;Adam
9;Mark
...
24;Mark
25;John
26;John
27;John
28;John
29;Adam
30;Adam
you could create a sequence function:
public static IEnumerable<string> GenerateSequence(IEnumerable<string> sequence, int groupSize)
{
var day = 1;
while (true)
{
foreach (var element in sequence)
{
for (var i = 0; i < groupSize; ++i)
{
yield return $"{day};{element}";
day++;
}
}
}
}
usage:
var workers = new List<string> { "John", "Adam", "Mark" };
var cleanDays = 3;
GenerateSequence(workers, cleanDays).Take(100).Dump();
I would do something like this:
var cleanDays = 6; // Number of days in each shift
var max = 30; // The amount of days the loop will run for
var count = workers.Count(); // The amount of workers
if(count == 0) return; // Exit If there are no workers
if(count == 1) cleanDays = max; //See '3.' in explanation (*)
for(var index = 0; index < max; index++){
var worker = (index / cleanDays ) % count;
var day = index % cleanDays ;
Console.WriteLine(string.format("Day {0}: {1} cleaned today (Consecutive days cleaned: {2})", index+1, workers[worker].Name ,day));
}
Explanation
By doing index / cleanDays you get the amount of times of worker shifts. But it is possible that the shifts are more than the amount of workers in which case you would want to get the reminder (shifts % amount of workers).
To get how many consecutive days the worker has worked so far you simply need to get the remainder of the first division done above. (index / cleanDays ).
Finally as you can see I get the count of the array before I enter the loop for 3 reasons:
To only read it once. And save some time.
To exit if the method if the array is empty
To check if there is only one worker left. In which case that worker won't have a break and will be working from day 1 until day 'max' therefore I set the cleanDays to max. *
I have a requirement to have a relative min/max date validation able to be stored in a database to customize an application per customer. I decided that the NodaTime.Period due to it's capability to specify years was the best choice. However, NodaTime.Period does not offer a way to compare itself against another period.
Example data provided for this validation:
Minimum Age of 18 years old.
Maximum Age o 100 years old.
Minimum sale duration of 1 month
Maximum sale duration of 3 months
Minimum advertising campaign 7 days
(Note: Current requirements are that Year / Month / Day will not be combined in validations)
The validations are:
public Period RelativeMinimum { get; set; }
public Period RelativeMaximum { get; set; }
Given a user entered date (and now):
var now = new LocalDate(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
var userValue = new LocalDate(date.Year, date.Month, date.Day);
var difference = Period.Between(userValue, now);
I have a comparison of:
if(RelativeMinimum != null && difference.IsLessThan(RelativeMinimum))))
{
response.IsValid = false;
response.Errors.Add(MinimumErrorMessage);
}
Which is consuming an extensions class:
public static class PeriodExtensions
{
public static bool IsLessThan(this Period p, Period p2)
{
return (p.Years < p2.Years) || (p.Years == p2.Years && p.Months < p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days < p2.Days);
}
public static bool IsGreaterThan(this Period p, Period p2)
{
return (p.Years > p2.Years) || (p.Years == p2.Years && p.Months > p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days > p2.Days);
}
}
While this approach works, given the test conditions I have, I have to wonder why #jon-skeet didn't implement this, and immediately have to worry over what am I missing and what alternative should I be using instead?
The main reason periods aren't comparable is that they can contain components of variable lengths.
Two one-month periods aren't necessarily the same number of days long. As an example, which is greater: 1 month or 30 days? If the month is January, then that's longer than 30 days. If the month is February, that's less than 30 days.
The same applies to years. Some are 365 days long, some are 366.
Of course, that all assumes you're using the Gregorian calendar. Noda Time supports other calendar systems, and there are similar quirks in those as well.
Regarding the code:
If you want a LocalDate from a DateTime, use LocalDateTime.FromDateTime(dt).Date
To get the current date, use SystemClock.Instance.Now.InZone(tz).Date
If you intended that to be the same as DateTime.Now, which uses the local time zone of the computer where the code is running, then get tz by calling DateTimeZoneProviders.Tzdb.GetSystemDefault()
For comparison of the type of problem you have described, consider defining min and max days instead of min and max periods. Then you wont have such variation of units. You can get the difference in days like this:
long days = Period.Between(d1, d2, PeriodUnits.Days).Days;
I believe something like this would work well for your use case:
public static bool IsDifferenceLessThan(LocalDate d1, LocalDate d2, Period p)
{
if (p.HasTimeComponent)
throw new ArgumentException("Can only compare dates.", "p");
if (p.Years != 0)
{
if (p.Months != 0 || p.Weeks != 0 || p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var years = Period.Between(d1, d2, PeriodUnits.Years).Years;
return years < p.Years;
}
if (p.Months != 0)
{
if (p.Weeks != 0 || p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var months = Period.Between(d1, d2, PeriodUnits.Months).Months;
return months < p.Months;
}
if (p.Weeks != 0)
{
if (p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var weeks = Period.Between(d1, d2, PeriodUnits.Weeks).Weeks;
return weeks < p.Weeks;
}
var days = Period.Between(d1, d2, PeriodUnits.Days).Days;
return days < p.Days;
}
Just as an additional point to Matt's already-excellent answer, we provide an option for creating an IComparer<Period> with a specific anchor point, e.g.
var febComparer = Period.CreateComparer(new LocalDate(2015, 2, 1).AtMidnight());
var marchComparer = Period.CreateComparer(new LocalDate(2015, 3, 1).AtMidnight());
var oneMonth = Period.FromMonths(1);
var twentyNineDays = Period.FromDays(29);
// -1: Feb 1st + 1 month is earlier than Feb 1st + 29 days
Console.WriteLine(febComparer.Compare(oneMonth, twentyNineDays));
// 1: March 1st + 1 month is later than March 1st + 29 days
Console.WriteLine(marchComparer.Compare(oneMonth, twentyNineDays));
I have a little calendar tool in C# and I am trying to figure out how to do a conversion from one array of DateTime objects to another. Here are the details:
I start off with collection of DateTime object
IEnumerable<DateTime> slots = GetSlots();
where each DateTime represents that starting time of an available slot (think open slot in calendar) All slots are for 30 minutes This is a given. So for example:
var slots = new List<DateTime>()
slots.Add(DateTime.Today + new TimeSpan(5,00, 0));
slots.Add(DateTime.Today + new TimeSpan(9,00, 0));
slots.Add(DateTime.Today + new TimeSpan(9,30, 0));
slots.Add(DateTime.Today + new TimeSpan(10,00, 0));
slots.Add(DateTime.Today + new TimeSpan(10,30, 0));
slots.Add(DateTime.Today + new TimeSpan(11,00, 0));
slots.Add(DateTime.Today + new TimeSpan(16,30, 0));
in the above example, it means i am free:
From 5:00 - 5:30
From 9:00 - 9:30
From 9:30 - 10:00
From 10:00 - 10:30
From 10:30 - 11:00
From 11:00 - 11:30
From 4:30 - 5:00
because i take the time from the item in the collection as the start time and simply add 30 minutes to it and that is considered a free slot.
I now have the requirement to take a larger time window (lets use 2 hours) and find out how many 2 hour slots free i have so I now need to take this array of dates and "merge" into into bigger buckets. Given the bigger bucket is 2 hours (120 minutes), I want a function like this
IEnumerable<DateTime> aggregateArray = MergeIntoLargerSlots(slots, 120);
I would basically have to loop through the slots array above and "merge" items that are lined up next to each out to make bigger buckets. If any of the merged items is 2 hours long then that should show up as an entry in the resulting array. Using the example above the resulting aggregateArray would have 2 items in the collection it that would have the times:
9AM (because i have a free slot from 9-11 AM (120 mins).
9:30AM (because i have a free slot from 9:30-11:30 AM (120 mins).
NOTE: 30 minutes "chunks" are the smallest interval so DON'T need to include 9:05 to 11:05 as an example
So given the previous array I have two 2 hour windows of time free in the day
I am struggling to figure out how this MergeIntoLargerSlots function would work so i would hoping to get some suggestion for how to approach this problem.
This only works for half hour intervals, you can figure out to make it work for others if you need to.
public List<DateTime> MergeIntoLargerSlots(List<DateTime> slots, int minutes)
{
int count = minutes/30;
List<DateTime> retVal = new List<DateTime>();
foreach (DateTime slot in slots)
{
DateTime end = slot.AddMinutes(minutes);
if (slots.Where(x => x >= slot && x < end).Count() == count)
{
retVal.Add(slot);
}
}
return retVal;
}
Here's a brief explanation of my problem solving approach; I take in the minutes and the slots list. I add minutes to get an end time which gives me range. From there, I use the Where operator to produce and IEnumerable<DateTime> from slots that has the slots in that range. I compare the result to the count variable I got from doing minutes/slotLength if the numbers match then you have the necessary slots. With your sample data the result of the Where for 9 AM would have 4 values in it; 9, 9:30, 10 and 10:30, ofc the count is 4, 120/30 == 4, so that gets added to retVal. The same would be true for 9:30, no other times would be returned.
Evan beat me to and did it with one less loop, but here was my solution:
private List<DateTime> MergeArray(List<DateTime> slots, int minutes)
{
var segments = minutes / InitialSegment;
var validSegments = new List<DateTime>();
foreach (var slot in slots.OrderBy(x => x))
{
var validSegment = true;
for (var i = 0; i < segments-1; i++)
{
var next = slot.AddMinutes(InitialSegment * (i + 1));
if (slots.All(x => x != next))
{
validSegment = false;
break;
}
}
if (validSegment)
validSegments.Add(slot);
}
return validSegments;
}
Assuming that your original list is sorted (if it is not, make it so it is), you could loop through your original list and check whether adjacent items are consecutive (i.e. whether the start times have a distance of exactly 30 minutes). Always keep track of the first item in the current series of consecutive timeslots - once you reach four of them (with 4 consecutive 30 minutes timeslots adding up to a possible two-hour timeslot; other timeslot sizes obviously require different factors), save a new two-hour timeslot into your resulting list and update your reference to the beginning of the current series of consecutive items.
Untested, so please consider this as pseudocode:
var twoHourSlots = new List<DateTime>();
int consecutiveSlotsCount = 0;
DateTime? previousSlot;
foreach (DateTime smallSlotStart in slots) {
if (previousSlot.HasValue) {
if (smallSlotStart - previousSlot.Value == new TimeSpan(0, 30, 0)) {
consecutiveSlotsCount++;
} else {
consecutiveSlotsCount = 0;
}
}
if (consecutiveSlotsCount == 4) {
twoHourSlots.Add(smallSlotStart - new TimeSpan(1, 30, 0));
consecutiveSlots = 0;
previousSlot = null;
} else {
previousSlot = smallSlotStart;
}
}
Some things to note:
I am using arithmetic operators on DateTime values. Check the docs to find out more; they do handy things and often let you work with TimeSpan values automatically.
I am using a TimeSpan constructor that takes hours, minutes and seconds several times. So that's what the three numbers mean.
I have declared previousSlot, a variable that keeps track of the last slot looked at (to compare to the current one), as DateTime? (again, check the docs if you are not sure what a nullable type is). That is because in the first iteration of the foreach loop, there is no previous slot to look at and the loop has to behave differently.
Likewise, previousSlot is set to null when we have found a 2-hour slot, as the last 30-minute slot of the found 2-hour slot should not be counted to the next possible 2-hour slot.
Once four consecutive 30-minute slots have been found, one hour and thirty minutes are subtracted from the beginning of the last one. That is because the thirty minutes after the beginning of the last 30-minute slot will be part of the resulting 2-hour slot.
I would create a TimeInterval class since there are a lot of other interesting things you can do with it.
public sealed class TimeInterval
{
public DateTime Start { get; private set; }
public DateTime End { get { return Start.AddMinutes(Duration); } }
public double Duration { get; private set; }
public TimeInterval(DateTime start, int duration)
{
Start = start;
Duration = duration;
}
public IEnumerable<TimeInterval> Merge(TimeInterval that)
{
if(that.Start >= this.Start && that.Start <= this.End)
{
if(that.End > this.End)
Duration += (that.Duration - (this.End - that.Start).TotalMinutes);
yield return this;
}
else
{
yield return this;
yield return that;
}
}
}
And this is an O(n) merge algorithm that will work for intervals of arbitrary sizes (in minutes).
//the `spans` parameter must be presorted
public IEnumerable<TimeInterval> Merge(IEnumerable<TimeInterval> spans, int duration)
{
var stack = new Stack<TimeInterval>();
stack.Push(spans.First());
foreach (var span in spans.Skip(1))
foreach(var interval in stack.Pop().Merge(span)) //this enumeration is guaranteed to have either one element or two elements.
stack.Push(interval);
return from interval in stack where interval.Duration >= duration select interval;
}