In analysis of energy demand and consumption data, I'm having issue re-sampling and interpolating time series trended data.
Data set example:
timestamp value kWh
------------------ ---------
12/19/2011 5:43:21 PM 79178
12/19/2011 5:58:21 PM 79179.88
12/19/2011 6:13:21 PM 79182.13
12/19/2011 6:28:21 PM 79183.88
12/19/2011 6:43:21 PM 79185.63
Based upon these observations, I'd like some aggregation to roll-up values based upon a period of time, with that frequency set to a unit of time.
As in, intervals on the hour filling any gaps of missing data
timestamp value (approx)
------------------ ---------
12/19/2011 5:00:00 PM 79173
12/19/2011 6:00:00 PM 79179
12/19/2011 7:00:00 PM 79186
For a linear algorithm, it seems I would take the difference in time and multiply the value against that factor.
TimeSpan ts = current - previous;
Double factor = ts.TotalMinutes / period;
Value and timestamp could be calculated based upon the factor.
With such quantity of available information, I'm unsure why it's difficult to find the most elegant approach to this.
Perhaps first, are there open source analysis libraries that could be recommended?
Any recommendations for a programmatic approach? Ideally C#, or possibly with SQL?
Or, any similar questions (with answers) I could be pointed to?
By using the time-ticks that are used internally to represent DateTimes, you get the most accurate values that are possible. Since these time ticks do not restart at zero at midnight, you will not have problems at day boundaries.
// Sample times and full hour
DateTime lastSampleTimeBeforeFullHour = new DateTime(2011, 12, 19, 17, 58, 21);
DateTime firstSampleTimeAfterFullHour = new DateTime(2011, 12, 19, 18, 13, 21);
DateTime fullHour = new DateTime(2011, 12, 19, 18, 00, 00);
// Times as ticks (most accurate time unit)
long t0 = lastSampleTimeBeforeFullHour.Ticks;
long t1 = firstSampleTimeAfterFullHour.Ticks;
long tf = fullHour.Ticks;
// Energy samples
double e0 = 79179.88; // kWh before full hour
double e1 = 79182.13; // kWh after full hour
double ef; // interpolated energy at full hour
ef = e0 + (tf - t0) * (e1 - e0) / (t1 - t0); // ==> 79180.1275 kWh
Explanation of the formula
In geometry, similar triangles are triangles that have the same shape but different sizes. The formula above is based on the fact that the ratios of any two sides in one triangle are the same for the corresponding sides of a similar triangle.
If you have a triangle A B C and a similar triangle a b c, then A : B = a : b. The equality of two ratios is called a proportion.
We can apply this proportionality rule to our problem:
(e1 – e0) / (t1 – t0) = (ef – e0) / (tf – t0)
--- large triangle -- --- small triangle --
I have written a LINQ function to interpolate and normalize time series data so that it can be aggregated/merged.
The Resample function is as follows. I have written a short article about this technique at the Code Project.
// The function is an extension method, so it must be defined in a static class.
public static class ResampleExt
{
// Resample an input time series and create a new time series between two
// particular dates sampled at a specified time interval.
public static IEnumerable<OutputDataT> Resample<InputValueT, OutputDataT>(
// Input time series to be resampled.
this IEnumerable<InputValueT> source,
// Start date of the new time series.
DateTime startDate,
// Date at which the new time series will have ended.
DateTime endDate,
// The time interval between samples.
TimeSpan resampleInterval,
// Function that selects a date/time value from an input data point.
Func<InputValueT, DateTime> dateSelector,
// Interpolation function that produces a new interpolated data point
// at a particular time between two input data points.
Func<DateTime, InputValueT, InputValueT, double, OutputDataT> interpolator
)
{
// ... argument checking omitted ...
//
// Manually enumerate the input time series...
// This is manual because the first data point must be treated specially.
//
var e = source.GetEnumerator();
if (e.MoveNext())
{
// Initialize working date to the start date, this variable will be used to
// walk forward in time towards the end date.
var workingDate = startDate;
// Extract the first data point from the input time series.
var firstDataPoint = e.Current;
// Extract the first data point's date using the date selector.
var firstDate = dateSelector(firstDataPoint);
// Loop forward in time until we reach either the date of the first
// data point or the end date, which ever comes first.
while (workingDate < endDate && workingDate <= firstDate)
{
// Until we reach the date of the first data point,
// use the interpolation function to generate an output
// data point from the first data point.
yield return interpolator(workingDate, firstDataPoint, firstDataPoint, 0);
// Walk forward in time by the specified time period.
workingDate += resampleInterval;
}
//
// Setup current data point... we will now loop over input data points and
// interpolate between the current and next data points.
//
var curDataPoint = firstDataPoint;
var curDate = firstDate;
//
// After we have reached the first data point, loop over remaining input data points until
// either the input data points have been exhausted or we have reached the end date.
//
while (workingDate < endDate && e.MoveNext())
{
// Extract the next data point from the input time series.
var nextDataPoint = e.Current;
// Extract the next data point's date using the data selector.
var nextDate = dateSelector(nextDataPoint);
// Calculate the time span between the dates of the current and next data points.
var timeSpan = nextDate - firstDate;
// Loop forward in time until wwe have moved beyond the date of the next data point.
while (workingDate <= endDate && workingDate < nextDate)
{
// The time span from the current date to the working date.
var curTimeSpan = workingDate - curDate;
// The time between the dates as a percentage (a 0-1 value).
var timePct = curTimeSpan.TotalSeconds / timeSpan.TotalSeconds;
// Interpolate an output data point at the particular time between
// the current and next data points.
yield return interpolator(workingDate, curDataPoint, nextDataPoint, timePct);
// Walk forward in time by the specified time period.
workingDate += resampleInterval;
}
// Swap the next data point into the current data point so we can move on and continue
// the interpolation with each subsqeuent data point assuming the role of
// 'next data point' in the next iteration of this loop.
curDataPoint = nextDataPoint;
curDate = nextDate;
}
// Finally loop forward in time until we reach the end date.
while (workingDate < endDate)
{
// Interpolate an output data point generated from the last data point.
yield return interpolator(workingDate, curDataPoint, curDataPoint, 1);
// Walk forward in time by the specified time period.
workingDate += resampleInterval;
}
}
}
}
Maby something like this:
SELECT DATE_FORMAT('%Y-%m-%d %H', timestamp) as day_hour, AVG(value) as aprox FROM table GROUP BY day_hour
What database engine you use?
for what you are doing it appears that you are declaring the TimeSpan incorrectly for starters ts = (TimeSpan)(current- previous); also make sure that current and previous are of DateTime type.
if you want to look at calculating or rolling up I would look at TotalHours() here is an example that you can look at for an idea if you like
here is check if a LastWrite / Modified time is within a 24 hour period
if (((TimeSpan)(DateTime.Now - fiUpdateFileFile.LastWriteTime)).TotalHours < 24){}
I know that this is different that your case but you get the drift on how to use TotalHours
Related
We need to floor a time to the nearest arbitrary interval (represented by e.g. a Timespan or a Duration).
Assume for an example that we need to floor it to the nearest ten minutes.
e.g. 13:02 becomes 13:00 and 14:12 becomes 14:10
Without using Nodatime you could do something like this:
// Floor
long ticks = date.Ticks / span.Ticks;
return new DateTime( ticks * span.Ticks );
Which will use the ticks of a timespan to floor a datetime to a specific time.
It seems NodaTime exposes some complexity we hadn't considered before. You can write a function like this:
public static Instant FloorBy(this Instant time, Duration duration)
=> time.Minus(Duration.FromTicks(time.ToUnixTimeTicks() % duration.BclCompatibleTicks));
But that implementation doesn't seem correct.
"Floor to nearest ten minutes" seems to be dependent on timezone/offset of the time.
While might be 13:02 in UTC, in Nepal which has an offset of +05:45, the time would be 18:47.
This means that in UTC, flooring to the nearest ten minutes, would mean subtracting two minutes, while in Nepal, it would mean subtracting seven minutes.
I feel like I should be able to round a ZonedDateTime or an OffsetDateTime by an arbitrary timespan somehow. I can get close by writing a function like this
public static OffsetDateTime FloorToNearestTenMinutes(this OffsetDateTime time)
{
return time
.Minus(Duration.FromMinutes(time.Minute % 10))
.Minus(Duration.FromSeconds(time.Second));
}
but that doesn't allow me to specify an arbitrary duration, as the OffsetDateTime has no concept of ticks.
How do I round an Instant/ZonedDateTime/OffsetDateTime correctly, with an arbitrary interval, taking into account time zones?
For OffsetDateTime, I'd advise you to write a Func<LocalTime, LocalTime> which is effectively an "adjuster" in Noda Time terminology. You can then just use the With method:
// This could be a static field somewhere - or a method, so you can use
// a method group conversion.
Func<LocalTime, LocalTime> adjuster =>
new LocalTime(time.Hour, time.Minute - time.Minute % 10, 0);
// The With method applies the adjuster to just the time portion,
// keeping the date and offset the same.
OffsetDateTime rounded = originalOffsetDateTime.With(adjuster);
Note that this only works because your rounding will never change the date. If you need a version that can change date as well (e.g. rounding 23:58 to 00:00 of the next day) then you'd need to get the new LocalDateTime and construct a new OffsetDateTime with that LocalDateTime and the original offset. We don't have a convenience method for that, but it's just a matter of calling the constructor.
ZonedDateTime is fundamentally trickier due to the reasons you've given. Right now, Nepal doesn't observe DST - but it might do so in the future. Rounding near the DST boundary could take you into an ambiguous or even skipped time, potentially. That's why we don't provide a similar With method for ZonedDateTime. (In your case it isn't likely, although it's historically possibly... with date adjusters you could easily end up in this situation.)
What you could do is:
Call ZonedDateTime.ToOffsetDateTime
Round the OffsetDateTime as above
Call OffsetDateTime.InZone(zone) to get back to a ZonedDateTime
You could then check that the offset of the resulting ZonedDateTime is the same as the original, if you wanted to detect weird cases - but you'd then need to decide what to actually do about them. The behaviour is fairly reasonable though - if you start with a ZonedDateTime with a time portion of (say) 01:47, you'll end up with a ZonedDateTime in the same time zone from 7 minutes earlier. It's possible that wouldn't be 01:40, if a transition occurred within the last 7 minutes... but I suspect you don't actually need to worry about it.
I ended up taking some stuff from Jon Skeets answer and rolling my own Rounder that takes in an arbitrary Duration to round with. (Which was one of the key things I needed, which is also why I'm not accepting that answer).
Per Jons suggestion I convert the Instant to an OffsetDateTime and apply the rounder, which takes in an arbitrary duration. Example and implementation is below:
// Example of usage
public void Example()
{
Instant instant = SystemClock.Instance.GetCurrentInstant();
OffsetDateTime offsetDateTime = instant.WithOffset(Offset.Zero);
var transformedOffsetDateTime = offsetDateTime.With(t => RoundToDuration(t, Duration.FromMinutes(15)));
var transformedInstant = transformedOffsetDateTime.ToInstant();
}
// Rounding function, note that it at most truncates to midnight at the day.
public static LocalTime RoundToDuration(LocalTime timeToTransform, Duration durationToRoundBy)
{
var ticksInDuration = durationToRoundBy.BclCompatibleTicks;
var ticksInDay = timeToTransform.TickOfDay;
var ticksAfterRounding = ticksInDay % ticksInDuration;
var period = Period.FromTicks(ticksAfterRounding);
var transformedTime = timeToTransform.Minus(period);
return transformedTime;
}
For anyone interested here is my implementation, which correctly accounts for the occasions we cross a day, and always rounds up (rather than floors):
public static class RoundingExtensions
{
private static readonly Duration OneDay = Duration.FromDays(1);
public static LocalTime RoundUpToDuration(this LocalTime localDateTime, Duration duration)
{
if (duration <= Duration.Zero) return localDateTime;
var ticksInDuration = duration.BclCompatibleTicks;
var ticksInDay = localDateTime.TickOfDay;
var ticksAfterRounding = ticksInDay % ticksInDuration;
if (ticksAfterRounding == 0) return localDateTime;
// Create period to add ticks to get to next rounding.
var period = Period.FromTicks(ticksInDuration - ticksAfterRounding);
return localDateTime.Plus(period);
}
public static OffsetDateTime RoundUpToDuration(this OffsetDateTime offsetDateTime, Duration duration)
{
if (duration <= Duration.Zero) return offsetDateTime;
var result = offsetDateTime.With(t => RoundUpToDuration(t, duration));
if (OffsetDateTime.Comparer.Instant.Compare(offsetDateTime, result) > 0) result = result.Plus(OneDay);
return result;
}
public static ZonedDateTime RoundUpToDuration(this ZonedDateTime zonedDateTime, Duration duration)
{
if (duration <= Duration.Zero) return zonedDateTime;
var odt = zonedDateTime.ToOffsetDateTime().RoundUpToDuration(duration);
return odt.InZone(zonedDateTime.Zone);
}
}
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;
}
I have a table of datetime and values. The value is a volume of a tank at the datetime. I want to calculate how much the tank filled / emptied in every hour during the last 10 days.
I measure the tank every 15 minutes, but maybe I will measure it every 20 minutes in the future. I am trying to calculate it using LINQ.
What I did is:
allDataView.RowFilter = String.Format(CultureInfo.InvariantCulture.DateTimeFormat,
"DateOfData < #{0}# AND DateOfData > #{1}#", DateTime.Now, DateTime.Now.AddDays((-1) * daysInterval));
allDataView.Sort = "DateOfData DESC";
// making the diff between every sample during the last daysInterval days and put it into a list with these differences.
var result = allDataView.Table.AsEnumerable().Zip(allDataView.Table.AsEnumerable().Skip(1), (row1, row2) => Convert.ToDouble(row1["Value"])-Convert.ToDouble(row2["Value"]));
But these are differences between every measurement. What I want is to calculate the differences between samples in round times during that days period.
For example:
9:50 0.5
10:05 1
10:20 2
10:35 2.5
10:50 3
11:05 5
Than I want to take the latest value that was in 11:00 (3) and take the last value that was in 10:00 (0.5) and to insert it 3-0.5 = 2.5 to a list and continue during that time period.
I want to do it using Linq. Appreciate any help how to implement it.
Thanks a lot.
I think I made it:
var groups = from row in allDataView.Table.AsEnumerable()
group row by new { Convert.ToDateTime(row["DateOfData"]).Hour, Convert.ToDateTime(row["DateOfData"]).Date } into g
select new
{
FirstDate = g.First()["DateOfData"],
FirstValue = g.First()["Value"],
LastDate = g.Last()["DateOfData"],
LastValue = g.Last()["Value"]
};
var result = groups.Select(grp => Convert.ToDouble(grp.FirstValue) - Convert.ToDouble(grp.LastValue));
I'ld recommend creating an sp to handle the grouping of the date time values. This should have the added bonus of allowing your query to run quicker, you shoukld then be able to pull the results out faster via linq.
I've been cracking my head over this algorithm for the past week and a half and i cant get it to work.
Basically i have an schedule (i know the Time value of the "borders")
and i have the red section (peoples movements in and out of the workplace). What i want is to know the time people spend at the workplace WITHIN their schedule, i dont care if they are there before or after work, or in the lunch break.
do you have any suggestions? on a mathematical theory or rule that i can apply here? or a similar problem you have seen you can point me to? i've been having a really hard time finding a solution. Any help would be appreciated.
For example:
Schedule:
7:30am (start) 12:00pm(lunchbreak)
1:30pm(endLunchBreak) 5:00pm(endOfWorkday)
People movements trough the day:
IN: 6:50am, OUT: 6:55am
IN: 7:00am, OUT: 11:45am
IN: 1:45pm, OUT: 5:05pm
So, my expected output would be a timespan of: 7:30 (it ignores time IN workplace outside of work schedule)
I would treat this as a state machine problem. There are four states: S+W+, S-W+, S+W-, S-W-.
Scheduled time corresponds to S+ states, worker present to W+ states. The objective is to add time in S+W+ to the intersection time.
The valid transitions are:
S+W+ End of schedule -> S-W+
S+W+ Worker leaves -> S+W-
S-W+ Start of schedule -> S+W+
S-W+ Worker leaves -> S-W-
S+W- End of schedule -> S-W-
S+W- Worker arrives -> S+W+
S-W- Start of schedule -> S+W-
S-W+ Worker arrives -> S-W+
Process events in time order, starting in state S-W-. If two events happen at the same time, process in either order.
On transition into S+W+, note the time. On transition out of S+W+, subtract the last noted time from the time of the transition, and add the result to the intersection time.
Break the day into 1440 one minute increments. This is your set space.
Set "S", the scheduled minutes, is a subset of that space.
Set "W", the amount of time spent on the job, is a subset of that space.
The intersection of "S" and "W" is the amount of time the person was there within their schedule (in minutes - convert to hh:mm per your needs).
Using other set algorithms you can find when they should have been there but weren't, etc.
You might want to look into using this library, but be careful, it completely ignores DateTime.Kind, is not time zone aware, and doesn't respect daylight saving time.
It is safe to use on Utc kinds.
Never use it on Local kinds.
If you use it on Unspecified kinds, make sure you understand what the context is. If it could possibly be a local time in some time zone that has DST, then your results may or may not be correct.
Other than that, you should be able to use its intersection function.
It sounds like LINQ should work well here. I've whipped up a short example, using my Noda Time library as it has better support for "time of day" than .NET, but you could adapt it if necessary.
The idea is basically that you have two collections of periods, and you're only interested in the intersection - you can find the intersection of any schedule period against any movement period - it's easy to discount periods that don't intersect by just using a 0-length period.
Here's the complete code, which does indeed give a total time of 7 hours and 30 minutes:
using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
class Test
{
static void Main()
{
var schedule = new List<TimePeriod>
{
new TimePeriod(new LocalTime(7, 30), new LocalTime(12, 0)),
new TimePeriod(new LocalTime(13, 30), new LocalTime(17, 0)),
};
var movements = new List<TimePeriod>
{
new TimePeriod(new LocalTime(6, 50), new LocalTime(6, 55)),
new TimePeriod(new LocalTime(7, 0), new LocalTime(11, 45)),
new TimePeriod(new LocalTime(13, 45), new LocalTime(17, 05))
};
var durations = from s in schedule
from m in movements
select s.Intersect(m).Duration;
var total = durations.Aggregate((current, next) => current + next);
Console.WriteLine(total);
}
}
class TimePeriod
{
private readonly LocalTime start;
private readonly LocalTime end;
public TimePeriod(LocalTime start, LocalTime end)
{
if (start > end)
{
throw new ArgumentOutOfRangeException("end");
}
this.start = start;
this.end = end;
}
public LocalTime Start { get { return start; } }
public LocalTime End { get { return end; } }
public Duration Duration { get { return Period.Between(start, end)
.ToDuration(); } }
public TimePeriod Intersect(TimePeriod other)
{
// Take the max of the start-times and the min of the end-times
LocalTime newStart = start > other.start ? start : other.start;
LocalTime newEnd = end < other.end ? end : other.end;
// When the two don't actually intersect, just return an empty period.
// Otherwise, return the appropriate one.
if (newEnd < newStart)
{
newEnd = newStart;
}
return new TimePeriod(newStart, newEnd);
}
}
The problem:
I am in process of implementing a scheduler for my advisor in school. The scheduler supposes to setup a 15 minutes interval time slot from 8:00 AM to 5:00 PM, Monday to Friday. In addition, the advisor will have to specify the start and end dates of the scheduler. The scheduler will also feature an option to specify if the 15 minutes time slot is not open. Meaning my advisor will be able to mark specific time slot as NOT AVAILABLE.
What I have so far:
I have created a simple class:
public class TimeSlot
{
public DateTime dateTime
{
get;
set;
}
public bool isAvailable
{
get;
set;
}
TimeSlot(DateTime dt, bool Avalible)
{
dateTime = dt;
isAvailable = Avalible;
}
}
The class basically represents an object for one time slot in the scheduler. I also have a list of time slots that keeps a list of the valid time slots:
List<TimeSlot> TSList = new List<TimeSlot>();
Note that a valid time slot means the following:
Date is within: Monday to Friday.
Time is within: 8:00 AM to 5:00 PM
Time slots are within: 15 minutes interval.
In addition, I have a method that fill in the TSList as the following:
private void button_Next_Click(object sender, RoutedEventArgs e)
{
/* Getting the values of fromDate and toDate from the GUI controls*/
DateTime fromDate = datePicker1.SelectedDate.Value;
DateTime toDate = datePicker2.SelectedDate.Value;
while (fromDate <= toDate)
{
/*This ensures that we only deal with days Monday to Friday*/
if (fromDate.DayOfWeek.ToString() != "Saturday" && fromDate.DayOfWeek.ToString() != "Sunday")
{
/*PROBLEM HERE!!*/
}
/*Updating fromDate: Incrementing fromDate by 1 day*/
fromDate = fromDate.AddDays(1);
}
}
Notes that I was only able to satisfy the first condition in my valid time slot conditions. Thus, I was only able to restrict the dates to be within Monday to Friday range.
The questions:
I am trying to achieve the missing two valid conditions for a time slot:
How to restrict the times to be only 8:00am to 5:00 pm?
How to make time slots separated by 15 minutes interval?
First, please use DayOfWeek.Saturday and DayOfWeek.Sunday for the comparision, converting to a string is not necessary...
Then just use a simple loop like
DateTime startSlot = fromDate.Date.AddHours(8); // Starts at 8:00AM
while (startSlot.Hour < 17) {
// Construct time slot class
startSlot = startSlot.AddMinutes(15);
}
This gives you startSlot values starting at 8:00am at every date ranging to 5pm (i.e. the last one is 4:45pm).
Why are you considering building this out of nothing?
Why are you not starting with one of the many calendar management programs that are available off the shelf? For example, Microsoft Outlook contains calendar and schedule management, and you can do all of what you describe, easily. It also integrates with other scheduling tools via .ICS files, it syncs with mobile devices, syncs with Google Calendar, and so on.
But there are lots of other options. Google Calendar is another obvious one.
I don't know why you would ever consider starting from scratch. Unless it's an academic exercise (and no, I don't mean that you work in academia), then you should use larger building blocks to start.
It's like building a structure, starting with sand and water, instead of pre-fabricated concrete block.
Just quick implementation. Let me know if you need some comments.
// Round interval
const int roundInterval = 15;
var remainder = fromDate.TimeOfDay.Minutes % roundInterval;
var curTime = remainder == 0 ? fromDate : fromDate.AddMinutes(roundInterval - remainder);
curTime = curTime.AddSeconds(-curTime.TimeOfDay.Seconds);
var delta = TimeSpan.FromMinutes(roundInterval);
while (curTime < toDate)
{
while (curTime.DayOfWeek == DayOfWeek.Saturday || curTime.DayOfWeek == DayOfWeek.Sunday)
{
curTime = curTime.Date.AddDays(1);
}
if (curTime.TimeOfDay.Hours < 8)
{
curTime = curTime.AddHours(8 - curTime.TimeOfDay.Hours);
curTime = curTime.AddMinutes(-curTime.TimeOfDay.Minutes);
continue;
}
if (curTime.TimeOfDay.Hours >= 17)
{
curTime = curTime.AddHours(24 - curTime.TimeOfDay.Hours);
curTime = curTime.AddMinutes(-curTime.TimeOfDay.Minutes);
continue;
}
TSList.Add(new TimeSlot(curTime, true));
curTime = curTime.Add(delta);
}
}
DateTime myScheduledTimeSlot = new DateTime(2010, 10, 26, 8, 45, 0);
// Use existing check to check day of week constraint...
// Check if the datetime falls on a correct minute boundary
switch (myScheduledTimeSlot.Minute)
{
case 0:
case 15:
case 30:
case 45:
// The time slot is valid
break;
default:
// The time slot is not valid
break;
}
It is pretty simple to check whether it falls in a 15 minute slot as you don't have weird boundaries keeping every hour identical. I'd recommend checking out Quart.NET if you want to save some time doing eventing/scheduling.