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);
}
}
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 am attempting to create a timesheet calculator which takes calculates the time an employee works and I am close, with one problem.
As I perform the calculation, I only want hours and minutes to display. I am able to get that done, but that causes an issue. If the employee punches out before a full minute is elapsed, that minute is not included in the calculation.
For example, if an emp punches in at 12:00:30 and punches out at 5:00:29, that last minute is not counted in the calculation, so the time shows as 4:59 instead of 5:00.
How do I get the calculation to be based on the hours and minutes and exclude seconds completely?
This is the code I have:
private void btnPunchOut_Click(object sender, EventArgs e)
{
DateTime stopTime = DateTime.Now;
lblPunchOutTime.Text = stopTime.ToShortTimeString();
TimeSpan timeWorked = new TimeSpan();
timeWorked = stopTime - startTime;
lblTimeWorked.Text = timeWorked.ToString(#"hh\:mm");
}
Use TimeSpan.TotalSeconds perhaps...And then add 30 seconds or more, before you convert it to hours by dividing by 3600.
As in
lblTimeWorked.Text = ((timeWorked.TotalSeconds+30)/3600).ToString("0.00") + " hours";
Use Timespan.TotalHours if you want the hours.
But if you want to be accurate, you should create a separate class dedicated to calculating the hours worked by a staff member. Then you can encapsulate lots of business rules in the dedicated class. Staff have entitlements and overtime, expenses or penalty rates - so this can get complex if done properly.
If you want a calculation that really ignores the seconds, the clearest way to accomplish that is to get rid of the seconds on both the start time and the end time. It might not seem accurate because it allows a difference of one second to become a difference of one minute. But that could still be a valid business rule, that you want to subtract according the the minutes that appeared on the clock rather than the actual elapsed seconds.
In other words,
1:00:01 is adjusted to 1:00:00.
1:00:59 is adjusted to 1:00:00.
1:01:00 is "adjusted" to 1:01:00.
1:01:01 is adjusted to 1:01:00.
You can accomplish that with an extension like this:
public static class TimespanExtensions
{
public static TimeSpan TrimToMinutes(this TimeSpan input)
{
return TimeSpan.FromMinutes(Math.Truncate(input.TotalMinutes));
}
}
(I'm sure there's a more efficient way of truncating the seconds, but at least this is clear.)
Now instead of having to figure out how to calculate the difference while rounding seconds or adding seconds, you just trim the seconds before calculating the difference. Here's a unit test:
[TestMethod]
public void NumberOfMinutesIgnoresSeconds()
{
var startTime = TimeSpan.FromSeconds(59).TrimToMinutes();
var endTime = TimeSpan.FromSeconds(60).TrimToMinutes();
Assert.AreEqual(1, (endTime - startTime).TotalMinutes);
}
One Timespan represents 59 seconds, and the next one is 60, or the first second of the next minute. But if you trim the seconds and then calculate the difference you get exactly one minute.
In the context of your code,
DateTime stopTime = DateTime.Now;
lblPunchOutTime.Text = stopTime.ToShortTimeString();
var timeWorked = stopTime.TrimToMinutes() - startTime.TrimToMinutes();
lblTimeWorked.Text = timeWorked.ToString(#"hh\:mm");
Using System.Windows.Form.Timer the interval is an int, which gives a maximum interval limit of around 25 days. I know I could create some arbitrary algorithm to start another timer once the limit is reached, but that's just daft.
MISLEADING-IGNORE-->So if I want to set it to around 29 days (2619609112.7228003) milliseconds?<--MISLEADING-IGNORE
EDIT:
The real question here is how can I set System.Windows.Form.Timer to a value higher than maxInt?
The purpose is that I need to set an interval from whenever to the first day of the next month, so it could be 28,29,30 or 31 days, and when that interval expires, calculate the interval to the next first day of the month.
(Basically a Crystal Report is to be run on the 1st day of the month and printed (around 500 pages), because of the length of the reports it is to be run out of hours so it doesn't tie up the printer.)
e.g. run it today (today is 1/12/15), 1/1/16 is next 'first day of the month' so set the interval to the milliseconds between now and then.
1/1/16 comes around so the timer ticks, then calculate and set the interval for 1/2/2016 (the next first day of the month).
#SeeSharp - I did see that question, but I am working on a legacy app and am unsure of the implications of changing the timer, but if I can't get this timer to work I may look at the threading one, thanks.
EDIT2: Thanks for all of your suggestions, I've opted for a 3rd party plugin called FluentScheduler
Set the timer interval to one day (say) and use it to count the number of days up to 29.
Edit
Set the timer to half a day (say) and use it to check that the date is the first of the month.
How about a Month timer - This will fire close to midnight when the month changes. May be that suits your requirement better ?
If we have to consider day-light saving too, then perhaps the timer should fire at 2:00 AM on the 1st day of month so I'll make it configurable.
Here is a code to explain my idea -
public class MonthTimer : IDisposable
{
public event EventHandler<MonthChangedEventArgs> MonthChanged;
DateTime mLastTimerDate;
Timer mTimer;
public MonthTimer(TimeSpan timeOfFirstDay)
: this(DateTime.Now, timeOfFirstDay)
{
}
public MonthTimer(DateTime currentDate, TimeSpan timeOfFirstDay)
{
mLastTimerDate = currentDate.Date;
var milliSecondsInDay = new TimeSpan(1, 0, 0, 0).TotalMilliseconds;
Contract.Assert(timeOfFirstDay.TotalMilliseconds <= milliSecondsInDay); // time within 1st day of month
DateTime currentDateLastSecond = currentDate.Date.AddDays(1).AddTicks(-1); // one tick before midnight
TimeSpan timeSpanInCurrentDate = currentDateLastSecond.Subtract(currentDate); // remaining time till today ends
// I want the timer to check every day at specifed time (as in timeOfFirstDay) if the month has changed
// therefore at first I would like timer's timeout to be until the same time, following day
var milliSecondsTillTomorrow = (timeSpanInCurrentDate + timeOfFirstDay).TotalMilliseconds;
// since out milliseconds will never exceed - . Its okay to convert them to int32
mTimer = new Timer(TimerTick, null, Convert.ToInt32(milliSecondsTillTomorrow), Convert.ToInt32(milliSecondsInDay));
}
private void TimerTick(object state)
{
if(DateTime.Now.Month != mLastTimerDate.Month)
{
if (MonthChanged != null)
MonthChanged(this, new MonthChangedEventArgs(mLastTimerDate, DateTime.Now.Date));
}
mLastTimerDate = DateTime.Now.Date;
}
public void Dispose()
{
mTimer.Dispose();
}
}
public class MonthChangedEventArgs : EventArgs
{
public MonthChangedEventArgs(DateTime previousMonth, DateTime currentMonth)
{
CurrentMonth = currentMonth;
PreviousMonth = previousMonth;
}
public DateTime CurrentMonth
{
get;
private set;
}
public DateTime PreviousMonth
{
get;
private set;
}
}
client code
// Check the new month around 2 AM on 1st day
mMonthTimer = new MonthTimer(new TimeSpan(2, 0, 0));
mMonthTimer.MonthChanged += mMonthTimer_MonthChanged;
One thing I'm not using System.Threading.Timer, therefor the even handler will be called on a separate thread & not UI thread as incase of System.Windows.Forms.Timer if this is an issue in yr case do let me know.
Also do write me a comment if it serves yr purpose or any if any issues
Try Microsoft's Reactive Framework (NuGet "Rx-Main").
You can write this:
Observable
.Timer(DateTimeOffset.Now.AddDays(29.0))
.Subscribe(x =>
{
/* 29 Days Later */
});
I wonder how I can get the duration between 2300 and 0100, which should be 0200, but it returns 2200. Im working on an application with Xamarin.Forms and use two TimePickers which returns a TimeSpan.
private TimeSpan CalculateDuration()
{
var result = timePickerEnd.Time.Subtract(timePickerStart.Time);
return result.Duration();
}
As long as the startTime is smaller then the endTime, everything works fine. But if someone starts something at 2300 and ends at 0100 it returns 22. I wonder if anyone have some guidelines how i should attack this problem.
You have specific rules, you have to implement them:
var ts1 = timePickerStart.Time;
var ts2 = timePickerEnd.Time;
var difference= ts2.Subtract(ts1);
if(ts1 > ts2)
{
difference= difference.Add(TimeSpan.FromHours(24));
}
return difference;
Because the rule that you've failed to articulate (that I've guessed at above) is that "if the start time is greater than the end time, then they should be interpreted as occurring on successive days" - which is by no means a universal assumption that the system should make.
In my project I can block internet access completely in specific times but in a current date.
I did this by using a timer. This timer checks the start time and end time and also controls in every 5 seconds to disable and enable internet connection by using the start time and end time given by user. Now I want to do the same thing in different days not only for the current date. I want to check every day in every 5 seconds of a day by timer if a disable/enable time interval is given by the user for that specific date.
if (currentDate == chosenDate)
{
if ((currenttime >= starttime) && (currenttime <= endtime))
{
timerForHour.Enabled = true;
if (isConnectedToNetwork)
{
findNetworkName();
Disable(networkName);
}
}
else
{
exitFlag = true;
Enable(networkName);
}
}
else if(currentDate!=chosenDate)
{
}
This is my code what am I going to do in this else if section?. Thanks.
I think you are going about this the wrong way. Don't think about it in terms of "if it's today or not", instead think about it in a more general way: "does the day match or not, if it does then are we within the time range acceptable?". Without knowing more about how you are storing the day / time ranges you are using I can offer the following using LINQ:
IsInternetAllowed = false;
DateTime dateOfInterest = datesToRestrict
.FirstOrDefault(dt => dt.Date == DateTime.Now.Date);
if (dateOfInterest != null)
{
if (timesToAllowDict.ContainsKey(DateTime.Now.Date))
{
List<Tuple<DateTime, DateTime>> lstAllowed =
timesToAllowDict[DateTime.Now.Date];
IsInternetAllowed = lstAllowed
.FirstOrDefault(time =>
DateTime.Now.Time > time.Value1 &&
DateTime.Now.Time < time.Value2) != null;
}
}
if (IsInternetAllowed)
Enable(networkName);
else
Disable(networkName);
I make the following assumptions about your data:
There are potentially multiple times allowed per day.
Any time that network access is not allowed it is disabled.
You store your allowed dates & times in a
Dictionary<DateTime, List<Tuple<DateTime, DateTime>>> structure.
That structure represents Date, List of times allowed (start then end).
Each day is added separately (one dictionary key per day allowed).
It is likely that with some streamlining you could remove the need to have a dictionary structure and only use the start & end times instead.