public static class MidnightPartitioner
{
public struct SchedulePartition
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
/// <summary>
/// Splits a starting and ending DateTime range into
/// SchedulePartition objects. Each SchedulePartition
/// represents a span of time spent in a specific date.
/// </summary>
/// <param name="scheduleStart"></param>
/// <param name="scheduleEnd"></param>
/// <returns></returns>
public static IEnumerable<SchedulePartition> Break(DateTime scheduleStart, DateTime scheduleEnd)
{
List<SchedulePartition> dayBlocks = new List<SchedulePartition>();
if (scheduleStart <= scheduleEnd)
{
DateTime tomorrow = scheduleStart.Date.AddDays(1); // Determine when tomorrow is.
if (tomorrow < scheduleEnd) /* Schedule ends after tomorrow */
{
// Calculates the time spent spent in the scheduleStart date.
TimeSpan timeSpentInScheduleStartDate = tomorrow.Subtract(scheduleStart);
// Calculates the time spent in the day after the schedule start date
TimeSpan timeSpentInDayAfterScheudleStart = scheduleEnd.Subtract(tomorrow);
dayBlocks.Add(
new SchedulePartition { Start = scheduleStart, End = scheduleStart.Add(timeSpentInScheduleStartDate) }
);
if (timeSpentInDayAfterScheudleStart.TotalDays > 1)
{
dayBlocks.AddRange(
Break(tomorrow, scheduleEnd)
);
}
else
{
dayBlocks.Add(
new SchedulePartition { Start = tomorrow, End = tomorrow.Add(timeSpentInDayAfterScheudleStart) }
);
}
}
else
{
/* Schedule ends before tomorrow. Cannot be partitioned around midnight.
* Return the uncut schedule as a SchedulePartition */
return new[]
{
new SchedulePartition
{
Start = scheduleStart,
End = scheduleEnd
}
};
}
}
else
throw new ArgumentException("Chronological error with scheduleStart and scheduleEnd arguments");
return dayBlocks;
}
}
Refactorings
No refactoring yet !
Ants
July 5, 2009, July 05, 2009 09:52, permalink
I refactored the code to use a loop instead of recursion.
public static class MidnightPartitioner
{
public struct SchedulePartition
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
static DateTime MinDateTime(DateTime a, DateTime b)
{
return a <= b ? a : b;
}
public static IEnumerable<SchedulePartition> Break(DateTime scheduleStart, DateTime scheduleEnd)
{
if (scheduleStart > scheduleEnd)
throw new ArgumentException("Chronological error with scheduleStart and scheduleEnd arguments");
List<SchedulePartition> dayBlocks = new List<SchedulePartition>();
DateTime tomorrow = scheduleStart.Date;
do
{
tomorrow = tomorrow.AddDays(1);
dayBlocks.Add(new SchedulePartition
{
Start = scheduleStart,
End = MinDateTime(tomorrow, scheduleEnd)
});
scheduleStart = tomorrow;
} while (scheduleStart < scheduleEnd);
return dayBlocks;
}
}
public class MidnightPartitionerFacts
{
[Fact]
public void ThrowsOnOutOfOrder()
{
DateTime start = new DateTime(2009, 6, 12, 17, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 9, 0, 0);
Assert.Throws<ArgumentException>(() => (MidnightPartitioner.Break(start, end)));
}
void AssertSum(DateTime start, DateTime end, IEnumerable<MidnightPartitioner.SchedulePartition> parts)
{
TimeSpan span = TimeSpan.Zero;
foreach (var part in parts)
{
Assert.True(part.End >= part.Start);
span += part.End - part.Start;
}
Assert.Equal(end - start, span);
}
[Fact]
public void HandlesNoTimeSpent()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime end = start;
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocks()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocksStartingAtMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocksEndingAtMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 17, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesBlocksMidnightToMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesShortBlocksAcrossMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 23, 59, 59);
DateTime midnight = new DateTime(2009, 6, 13, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 1);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(2, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(midnight, parts[0].End);
Assert.Equal(midnight, parts[1].Start);
Assert.Equal(end, parts[1].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesBlocksAcrossTwoMidnights()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime midnight1 = new DateTime(2009, 6, 13, 0, 0, 0);
DateTime midnight2 = new DateTime(2009, 6, 14, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 14, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(3, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(midnight1, parts[0].End);
Assert.Equal(midnight1, parts[1].Start);
Assert.Equal(midnight2, parts[1].End);
Assert.Equal(midnight2, parts[2].Start);
Assert.Equal(end, parts[2].End);
AssertSum(start, end, parts);
}
}
Ants
July 5, 2009, July 05, 2009 10:25, permalink
A variation of the above implementation. This flavor gets rid of the dayBlocks list by using the compiler support for IEnumerable.
Additionally, the partEnd variable introduced in line 23 will make it easier to change the block sizes in the future, by simply changing how the tomorrow variable is initialized and incremented.
public static class MidnightPartitioner
{
public struct SchedulePartition
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
static DateTime MinDateTime(DateTime a, DateTime b)
{
return a <= b ? a : b;
}
public static IEnumerable<SchedulePartition> Break(DateTime scheduleStart, DateTime scheduleEnd)
{
if (scheduleStart > scheduleEnd)
throw new ArgumentException("Chronological error with scheduleStart and scheduleEnd arguments");
DateTime tomorrow = scheduleStart.Date;
do
{
tomorrow = tomorrow.AddDays(1);
DateTime partEnd = MinDateTime(tomorrow, scheduleEnd);
yield return new SchedulePartition { Start = scheduleStart, End = partEnd };
scheduleStart = partEnd;
} while (scheduleStart < scheduleEnd);
}
}
public class MidnightPartitionerFacts
{
[Fact]
public void ThrowsOnOutOfOrder()
{
DateTime start = new DateTime(2009, 6, 12, 17, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 9, 0, 0);
Assert.Throws<ArgumentException>(() => MidnightPartitioner.Break(start, end).GetEnumerator().MoveNext());
}
void AssertSum(DateTime start, DateTime end, IEnumerable<MidnightPartitioner.SchedulePartition> parts)
{
TimeSpan span = TimeSpan.Zero;
foreach (var part in parts)
{
Assert.True(part.End >= part.Start);
span += part.End - part.Start;
}
Assert.Equal(end - start, span);
}
[Fact]
public void HandlesNoTimeSpent()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime end = start;
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocks()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocksStartingAtMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 12, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesSameDayBlocksEndingAtMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 17, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesBlocksMidnightToMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(1, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(end, parts[0].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesShortBlocksAcrossMidnight()
{
DateTime start = new DateTime(2009, 6, 12, 23, 59, 59);
DateTime midnight = new DateTime(2009, 6, 13, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 13, 0, 0, 1);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(2, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(midnight, parts[0].End);
Assert.Equal(midnight, parts[1].Start);
Assert.Equal(end, parts[1].End);
AssertSum(start, end, parts);
}
[Fact]
public void HandlesBlocksAcrossTwoMidnights()
{
DateTime start = new DateTime(2009, 6, 12, 9, 0, 0);
DateTime midnight1 = new DateTime(2009, 6, 13, 0, 0, 0);
DateTime midnight2 = new DateTime(2009, 6, 14, 0, 0, 0);
DateTime end = new DateTime(2009, 6, 14, 17, 0, 0);
var parts = new List<MidnightPartitioner.SchedulePartition>(MidnightPartitioner.Break(start, end));
Assert.Equal(3, parts.Count);
Assert.Equal(start, parts[0].Start);
Assert.Equal(midnight1, parts[0].End);
Assert.Equal(midnight1, parts[1].Start);
Assert.Equal(midnight2, parts[1].End);
Assert.Equal(midnight2, parts[2].Start);
Assert.Equal(end, parts[2].End);
AssertSum(start, end, parts);
}
}
I have this little class called MidnightPartitioner which I use to split schedules (represented by a DateTime starting and ending) around midnight. I'm pretty sure my logic is correct in my code, but I have the feeling there is a cleaner and more concise way to split my schedule start and end times into 'blocks' around midnight. Any ideas on how I can clean up this code would be appreciated.
Example:
If scheduleStart and scheduleEnd spans 3 days, then the amount of time spent in each day would make give us 3 SchedulePartition objects. Thus, the TimeSpan length of all schedule partition objects should equal the TimeSpan of scheduleEnd.Subtract(scheduleStart)