Rounding time in Nodatime to nearest interval - c#

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);
}
}

Related

Better way to get Interval bounds when only time is given

I have to find out the limits of interval i.e upper-bound and lower-bound of an interval based on interval type when datetime is given.
Example: say given time = 12:05 (then, this lies in the interval range 12:00 - 1:00 if interval type is hourly; 12:00 - 12:30 if interval type is half-an hour based;
12:00 - 12:15 if interval type is quarterly. likewise interval type can be anything.
Currently i am loading all different set of interval ranges in a dictionary object on an application load and then i fetch interval range from this dictionary for the given time.
Sorry, I know this problem statement looks simple but couldn't think of other approaches as of now. It would be helpful if someone can help me here. Thanks in advance.
You can calculate the range start by dividing the total minutes by your interval and then subtracting the remainder from the total minutes. After that, you can easily get the end of the range.
First, you need to get the time part from your DateTime object as TimeSpan by using DateTime.TimeOfDay. Then use TimeSpan.TotalMinutes.
Here's a good start:
public class TimeRange
{
public TimeRange(TimeSpan from, TimeSpan to)
{
From = from;
To = to;
}
public TimeSpan From { get; set; }
public TimeSpan To { get; set; }
}
public TimeRange GetRange(DateTime d, int minutesInterval)
{
TimeSpan time = d.TimeOfDay;
var from = time.TotalMinutes - (time.TotalMinutes % minutesInterval);
var to = from + minutesInterval;
return new TimeRange(TimeSpan.FromMinutes(from), TimeSpan.FromMinutes(to));
}
For clarity, I created a simple class called TimeRange to represent the start and end of the interval range. You can, however, feel free to handle this in a different way.
Usage:
DateTime d = DateTime.Now;
TimeRange range = GetRange(d, 60);
//TimeRange range = GetRange(d, 15);
Console.WriteLine("From: {0}\r\nTo: {1}", range.From, range.To);
Try it online.

Calculating time with hours and minutes only

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");

A way to get the number of milliseconds in real time passed since the last call of a method

I'm calling an update function to draw a real time simulation and was wondering if there was an effective way to get the number of milliseconds passed since the last update? At the moment I have a DispatchTimer calling at regular intervals to update the simulation but the timing isn't accurate enough and ends up being about 60% slower than it should be (it varies).
I would use Stopwatch.GetTimestamp() to get a tick count, then compare the value before and after. You can convert this to timings by:
var startTicks = Stopwatch.GetTimestamp();
// Do stuff
var ticks = Stopwatch.GetTimestamp() - startTicks;
double seconds = ticks / Stopwatch.Frequency;
double milliseconds = (ticks / Stopwatch.Frequency) * 1000;
double nanoseconds = (ticks / Stopwatch.Frequency) * 1000000000;
You could also use var sw = Stopwatch.StartNew(); and sw.Elapsed.TotalMilliseconds afterwards if you just want to time different chunks of code.
Keep a variable that will not reset between calls.
Yours may not need to be static like mine.
private static DateTime _LastLogTime = DateTime.Now;
Then within the method:
// This ensures only the exact one Tick is used for subsequent calculations
// Instead of calling DateTime.Now again and getting different values
DateTime NewTime = DateTime.Now;
TimeSpan ElapsedTime = NewTime - _LastLogTime;
_LastLogTime = NewTime;
string LogMessage = string.Format("{0,7:###.000}", ElapsedTime.TotalSeconds);
I only needed down to the thousandth of a second within my string, but you can get much more accurate with the resulting TimeSpan.
Also there is a .TotalMilliseconds or even .Ticks(the most accurate) value available within the resulting TimeSpan.

Calculate timespan between 2300 and 0100

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.

intersection of two sets of data

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);
}
}

Categories

Resources