I have an Outlook VSTO addin and I am doing search for resource calendar availability using the GetFreeBusy() API Calll which, given a date, will search over the next 28 days in 30 minute increments (by default) to determine which slots are free and which are busy. It works fine except I am struggling to figure out how to cope with the situation where a daylight savings time exists within that 28 day interval.
Here is my code:
using Microsoft.Office.Interop.Outlook;
string freeBusy = exchangeUser.GetFreeBusy(startDate, 30, true);
this gives me back a string like this that returns free / busy availability in 30 minute increments for 28 days.
0000000000000000202222222222000000000000000000000000000000000222222
this string is always 1344 characters long (48 slots per day * 28 days)
where each character represents a 30 minute slot and shows 0 if the time is free. I have the following parsing code that I took from this Microsoft article that returns an array of free time slots:
private IEnumerable<DateTime> ParseFreeBusy(string freeBusyString, DateTime startingDate)
{
var timeSlots = new List<DateTime>();
for (int i = 0; i < freeBusyString.Length; i++)
{
double slot = i * 30;
DateTime timeSlot = startingDate.Date.AddMinutes(slot);
bool isFree = freeBusy.Substring(i, 1) == "0";
if (isFree)
{
timeSlots.Add(timeSlot);
}
}
return timeSlots;
}
If I plug in October 25th as the start date when I look at the results every thing lines up perfectly up until November 2nd at 2AM (given daylight savings)
The root issue is that my naive code is simply increments and keeps adding 30 minutes for each entry since I am simply looping through each slot and doing this:
startingDate.Date.AddMinutes(slot);
I did a test and booked a calendar slot from 1AM - 2AM on November 2nd and this is what i get from GetFreeBusy() starting on that day
002222000...
so using the default loop above (remember, every character is a 30 min slot and 0 = free), this would translate to the following slot logic:
12L00 AM - free (0)
12:30 AM - free (0)
1L00 AM - booked (2)
1:30 AM - booked (2)
THESE NEXT TWO "booked" below is really representing the 2nd 1AM - 2AM since we roll the clocks back an hour
2:00 AM - booked (2)
2:30 AM - booked (2)
3:00 AM - free (0)
which is wrong as my code would show 2AM - 3AM booked when the "real" 2-3A AM is free. If my parsing was correct and handled this rollback, I would end up with this correct answer of:
12L00 AM - free (0)
12:30 AM - free (0)
1L00 AM - booked (2)
1:30 AM - booked (2)
IGNORE the second 1AM to 2AM as its already taken care of
2:00 AM - free (0)
2:30 AM - free (0)
3:00 AM - free (0)
What is interesting is that regardless of daylight savings, the resulting string is always 1344 characters long (I would have expected it to be shorter or longer on those months with daylight savings implications).
Does anyone have any experience with using outlook GetFreeBusy() and understand for how to deal with this situation when you hit a daily savings time slot?
I have been playing around with a few ideas like:
var tzInfo = TimeZoneInfo.Local;
if (tzInfo.IsAmbiguousTime(timeSlot))
{
//this would be a time to do something
}
or something like
DaylightTime daylightTime = tz.GetDaylightChanges(minStartTime.Year);
if (daylightTime.End == proposedTimeSlot)
{
daylightSavingsOffset = daylightSavingsOffset + ((daylightTime.Delta.Hours * 60) / meetingDuration);
}
but I am not completely sure what do with this once i detect the "special slots" and I can't find any documentation or recommendations around this situation.
Any suggestions?
What is interesting is that regardless of daylight savings, the resulting string is always 1344 characters long (I would have expected it to be shorter or longer on those months with daylight savings implications).
It's completely logical, Let's start with GetFreeBusy, It happens because the result is based on duration and specific intervals not Date and time stamps, and as we know Date and Time is relative to our location based on time zone but elapsed time and duration is not,let's assume we have meeting in 10 hours from now, we maybe are in different time Zones but after 10 hours (relative to our location) we both should meet each other, but our local times may vary significantly, the system works this way because it should be able to operate across different time zones at the same time, so it uses UniversalTime at the heart and converts it back to local time for generating the result.
Now let's check the code, when we use startingDate.Date.AddMinutes(slot); we are not considering DateTimeSaving as we are operating on our local time and the addition is relative to it, by using UniversalTime we can create a unified base point for our time additions and intervals, after that by converting it back to local time we can apply date time saving to it,
so I believe this code should work as expected:
private static IEnumerable<DateTime> ParseFreeBusy(string freeBusyString, DateTime startingDate)
{
var timeSlots = new HashSet<DateTime>();
var utc = startingDate.ToUniversalTime();
var timeZone = TimeZone.CurrentTimeZone; //can change to particular time zone, currently set to local timezone of the system
for (int i = 0; i < freeBusyString.Length; i++)
{
double slot = i * 30;
DateTime timeSlot = utc.AddMinutes(slot);
bool isFree = freeBusyString.Substring(i, 1) == "0";
if (isFree)
{
var localTimeSlot = timeZone.ToLocalTime(timeSlot);
timeSlots.Add(localTimeSlot);
}
}
return timeSlots;
}
NOTE:: beside using UTC for time, I changed the List to HashSet because if you have free slot on those specific times you would get duplicate entries, by using HashSet this problem won't occur.
here is a method I used for testing it:
private static void TestFreeSlots()
{
var saving = TimeZone.CurrentTimeZone.GetDaylightChanges(DateTime.Now.Year);
var datetime = new DateTime(saving.End.Year, saving.End.Month, saving.End.Day - 1);
//you may need to change the string to see effective result
var result = ParseFreeBusy("0000000000000000000000000000000000000000000000002222000", datetime);
}
and finally here is a little sample to demonstrate the method used here
private static void TestTimeZone()
{
var saving = TimeZone.CurrentTimeZone.GetDaylightChanges(DateTime.Now.Year);
var datetime = new DateTime(saving.End.Year, saving.End.Month, saving.End.Day - 1);
var utc = datetime.ToUniversalTime();
var timeZone = TimeZone.CurrentTimeZone;
for (var i = 0; i < 120; i++)
{
var next = timeZone.ToLocalTime(utc);
Console.WriteLine(next);
utc = utc.AddMinutes(30);
}
}
and your results should be similar to this:
This first function finds time slots that Outlook will return as duplicates due to DST. It can probably stand some refactoring but it's effective for now: (EDIT: I modified the function so it doesn't remove time slots as you go into DST).
public static Collection<DateTime> GetDuplicateSlots(
TimeZoneInfo timeZone, DateTime start, int intervalLength, int numOfIntervals)
{
Collection<DateTime> duplicates = new Collection<DateTime>();
bool dstAtStart = timeZone.IsDaylightSavingTime(start);
for (int interval = 0; interval < numOfIntervals; interval++)
{
DateTime current = start.Date.AddMinutes(interval * intervalLength);
if (dstAtStart && !timeZone.IsDaylightSavingTime(current))
{
duplicates.Add(current);
duplicates.Add(current.AddMinutes(intervalLength));
return duplicates;
}
}
return duplicates; // no duplicates
}
Then we just need to adjust for the duplicates when we go through the string of free/busy time slots:
public static void DisplayFreeBusy(
string freeBusyString, DateTime start, int intervalLength)
{
TimeZoneInfo cst = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
Collection<DateTime> duplicateSlots =
GetDuplicateSlots(cst, start, intervalLength, freeBusyString.Length);
int duplicatesConsumed = 0;
for (int slot = 0; slot < freeBusyString.Length; slot++)
{
int actualSlot = slot - duplicatesConsumed;
DateTime slotTime = start.Date.AddMinutes(actualSlot * intervalLength);
if (duplicatesConsumed != duplicateSlots.Count &&
duplicateSlots.Contains(slotTime))
{
duplicatesConsumed++;
}
else
{
Console.WriteLine("{0} -- {1}", slotTime, freeBusyString[slot]);
}
}
}
Note that the actualSlot variable corresponds to the time slots, while the slot variable still corresponds with a character in the free/busy string. When a duplicate is found, it is "consumed" and that character in the string is skipped. Once the duplicates have been consumed, the function will continue normally from that point.
I live in Arizona and we don't have DST so I had to force a different time zone. You can obviously substitute your local time zone instead of CST.
I tested this with a shorter input string but I added the extra '2' characters for the daylight savings slots. It handled the excess slots and prints out the proper number of slots.
Related
I have tried to write multiple programs relating to date/time (including Random.Next()), but I have a persisting problem. Every time I use a Console.ReadLine() statement (or some other statement that would cause the code to pause and resume) it refreshes the variable set by date/time as well as every variable caused by or modified by date/time in any way. Is there a way to store the time the program is run instead of the current time? (I'm using an online editor, dotnetfiddle.net, if that changes anything)
Here's a few examples:
using System;
namespace timeTest
{
public class Program
{
public static void Main()
{
Random random = new Random();//Random is based off of time, which updates
Console.WriteLine("Randomizer: " + random.Next(1,11));//Prints random number
long time = DateTime.Now.Ticks;
Console.WriteLine("Time in ticks: "+time);//Prints time in ticks
int hr = DateTime.Now.Hour;
int min = DateTime.Now.Minute;
int sec = DateTime.Now.Second;
Console.WriteLine("Military time: "+hr+":"+min+":"+sec);//Prints time on the clock
int sec2 = sec*2;//Stores value in a different variable times two
Console.WriteLine("2x the current second: "+sec2);//Prints twice the second
while (true) {Console.ReadLine();}//Every time Console.ReadLine is called, it changes again
}
}
}
DateTime.Now is a calculated property. It returns the current time at the exact moment it is called. When you run under the debugger, it always gets the value it has at the moment the value is evaluated by the debugger. Your program won't notice that, but it is something you need to keep in mind.
However, you have a conceptual error here:
int hr = DateTime.Now.Hour;
int min = DateTime.Now.Minute;
int sec = DateTime.Now.Second;
Because DateTime.Now is evaluated each time it is called, this returns a new instance of the time with every call. This may result in the minute not matching the hour displayed (when the hour just changes between the first and the second line, your displayed time will be an hour off).
Evaluate the value once:
DateTime now = DateTime.Now;
int hr = now.Hour;
int min = now.Minute;
int sec = now.Second;
You'll note that in this case, the value of now won't change, even if you stop in the debugger.
Note that there are also standard-formatting options that don't require you to separate the values yourself, just do
Console.WriteLine($"{now:T}"); // standard (localized) time format
This question already has answers here:
Timer callback raised every 24 hours - is DST handled correctly?
(3 answers)
Closed 5 years ago.
I'm not in front of my code right now, but don't really think I need it to ask this question. So I have a countdown timer that goes off every 18 hours and then resets. The timer checks the current DateTime.Now and adjusts the countdown timer as needed. I am having an issue trying to account for when daylight savings when it goes back an hour because; for example this past event on November 5th 2017 at 2am it goes back to 1am but when I do DateTime.IsDaylightSavingTime() it tells me that it's false even though Daylight Saving Time just went off. This makes my timer go back an extra hour because it thinks that Daylight Saving Time still hasn't happen for that one hour period. How would I get around this?
If you realy need to use local time for some reason, than you should count for DST changes in advance (prior scheduling next event).
TimeZoneInfo.GetAdjustmentRules() should help you to get the time and delta of next DST adjustment.
Following is the code for getting upcomming adjustment:
public static DateTime? GetNextAdjustmentDate(TimeZoneInfo timeZoneInfo)
{
var adjustments = timeZoneInfo.GetAdjustmentRules();
if (adjustments.Length == 0)
{
return null;
}
int year = DateTime.UtcNow.Year;
TimeZoneInfo.AdjustmentRule adjustment = null;
foreach (TimeZoneInfo.AdjustmentRule adjustment1 in adjustments)
{
// Determine if this adjustment rule covers year desired
if (adjustment1.DateStart.Year <= year && adjustment1.DateEnd.Year >= year)
adjustment = adjustment1;
}
if (adjustment == null)
return null;
//TimeZoneInfo.TransitionTime startTransition, endTransition;
DateTime dstStart = GetCurrentYearAdjustmentDate(adjustment.DaylightTransitionStart);
DateTime dstEnd = GetCurrentYearAdjustmentDate(adjustment.DaylightTransitionEnd);
if (dstStart >= DateTime.UtcNow.Date)
return dstStart;
if (dstEnd >= DateTime.UtcNow.Date)
return dstEnd;
return null;
}
private static DateTime GetCurrentYearAdjustmentDate(TimeZoneInfo.TransitionTime transitionTime)
{
int year = DateTime.UtcNow.Year;
if (transitionTime.IsFixedDateRule)
return new DateTime(year, transitionTime.Month, transitionTime.Day);
else
{
// For non-fixed date rules, get local calendar
System.Globalization.Calendar cal = CultureInfo.CurrentCulture.Calendar;
// Get first day of week for transition
// For example, the 3rd week starts no earlier than the 15th of the month
int startOfWeek = transitionTime.Week * 7 - 6;
// What day of the week does the month start on?
int firstDayOfWeek = (int)cal.GetDayOfWeek(new DateTime(year, transitionTime.Month, 1));
// Determine how much start date has to be adjusted
int transitionDay;
int changeDayOfWeek = (int)transitionTime.DayOfWeek;
if (firstDayOfWeek <= changeDayOfWeek)
transitionDay = startOfWeek + (changeDayOfWeek - firstDayOfWeek);
else
transitionDay = startOfWeek + (7 - firstDayOfWeek + changeDayOfWeek);
// Adjust for months with no fifth week
if (transitionDay > cal.GetDaysInMonth(year, transitionTime.Month))
transitionDay -= 7;
return new DateTime(year, transitionTime.Month, transitionDay);
}
}
You'd need to add some more code to retrieve and apply the adjustment delta.
Well - now, when you see all the hard work that would need to be done (and than maintained and made sure to be bug free), you might want to rething your problem to be able to use UTC.
I would use DateTime.UtcNow you won't have the issue with daylight savings
I feel like this is something really simple, but my Google Fu is letting me down as I keep finding difference calculations.
I have a time (e.g. 1800 hours) stored in a DateTime object. The date is null and immaterial. All I want to know is how many milliseconds until the NEXT occurrence of that time.
So, if I run the calculation at 0600 - it will return 12 hours (in ms). At 1750, it will return ten minutes (in ms) and at 1900 it will return 24 hours (in ms).
All the things I can find show me how to calculate differences, which doesn't work once you're past the time.
Here is what I tried, but fails once you're past the time and gives negative values:
DateTime nowTime = DateTime.Now;
TimeSpan difference = _shutdownTime.TimeOfDay - nowTime.TimeOfDay;
double result = difference.TotalMilliseconds;
You're already doing everything you should, except for one thing: handling negative results.
If the result is negative, it means the time you want to calculate the duration until has already passed, and then you want it to mean "tomorrow" instead, and get a positive value.
In this case, simply add 24 hours:
DateTime nowTime = DateTime.Now;
TimeSpan difference = _shutdownTime.TimeOfDay - nowTime.TimeOfDay;
double result = difference.TotalMilliseconds;
if (result < 0)
result += TimeSpan.FromHours(24).TotalMilliseconds;
The next thing to consider is this: If the time you want to calculate the duration until is 19:00 hours, and the current time is exactly 19:00 hours, do you want it to return 0 (zero) or 24 hours worth of time? Meaning, do you really want the next such occurrence?
If so, then change the above if-statement to use <=:
DateTime nowTime = DateTime.Now;
TimeSpan difference = _shutdownTime.TimeOfDay - nowTime.TimeOfDay;
double result = difference.TotalMilliseconds;
if (result <= 0)
result += TimeSpan.FromHours(24).TotalMilliseconds;
However, note that this will be prone to the usual problems with floating point values. If the current time is 18:59:59.9999999, do you still want it to return the current time (a minuscule portion of time) until 19:00 today, or do you want it to flip to tomorrow? If so, change the comparison to be slightly different:
DateTime nowTime = DateTime.Now;
TimeSpan difference = _shutdownTime.TimeOfDay - nowTime.TimeOfDay;
double result = difference.TotalMilliseconds;
if (result <= -0.0001)
result += TimeSpan.FromHours(24).TotalMilliseconds;
where -0.0001 is a value that corresponds to "the range of inaccuracy you're prepared to accept being tomorrow instead of today in terms of milliseconds".
When doing calculations like this it is important to take possible DST changes under consideration so that your results remain correct.
Suppose your operational parameters are:
var shutdownTime = TimeSpan.FromHours(18);
// just to illustrate, in Europe there is a DST change on 2013-10-27
// normally you 'd just use DateTime.Now here
var now = new DateTime(2013, 10, 26, 20, 00, 00);
// do your calculations in local time
var nextShutdown = now.Date + shutdownTime;
if (nextShutdown < now) {
nextShutdown = nextShutdown.AddDays(1);
}
// when you want to calculate time spans in absolute time
// (vs. wall clock time) always convert to UTC first
var remaining = nextShutdown.ToUniversalTime() - now.ToUniversalTime();
Console.WriteLine(remaining);
The answer to your question would now be remaining.TotalMilliseconds.
long ticks = new DateTime(2012, 1, 31).ToLocalTime().Ticks; // 634635684000000000
But how to do this without DateTime constructor ?
edit
What I actually want is to keep only the years, months and days from the ticks.
long ALL_Ticks = DateTime.Now.Ticks; // 634636033446495283
long Only_YearMonthDay = 634635684000000000; // how to do this ?
I want to use this in a linq-sql query using Linq.Translations.
If you only want the ticks for the date portion of the current datetime you could use:
long Only_YearMonthDay = DateTime.Now.Date.Ticks; //634635648000000000
//DateTime.Now.Date.Ticks + DateTime.Now.TimeOfDay.Ticks == DateTime.Now.Ticks
You could find out how many days are in the calculation and then multiply by 864,000,000,000 (which is how many ticks are in a day). Is that what you are looking for? Bit of documentation here : http://msdn.microsoft.com/en-us/library/system.timespan.ticksperday.aspx.
Happy coding,
Cheers,
Chris.
OK - didn't think this through properly! Ticks represent the amount of 100 nanosecond intervals since 12:00:00 midnight, January 1, 0001. You would need to calculate how many days have passed since that date and then multiply it by the ticks per day value!
If I understand you right, you are not worried about the ticks up to a particular time of the day?! So, it would be something along the lines of :
var ticksToDate = (DateTime.UtcNow - DateTime.MinValue).Days * 864000000000;
Does that answer your question??
That is going to be rather difficult unless you have some other way of getting the current date and time. According to MSDN:
A single tick represents one hundred nanoseconds or one ten-millionth of a second. There are 10,000 ticks in a millisecond.
The value of this property represents the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001, which represents DateTime.MinValue. It does not include the number of ticks that are attributable to leap seconds.
Now, if you know the current date and time, you can calculate how many days have passed since January 1, 0001 and use that to calculate the number of ticks.
I understand you dont want the hour parts of the date. If you use Date, then you only get the day (for example: 01/01/2012 00:00:00)
long ticks = new DateTime(2012, 1, 31).Date.Ticks;
And with any DateTime object already created is the same of course.
long ticks = dateObject.Date.Ticks;
You already have the answer there in your post:
long ALL_Ticks = DateTime.Now.Ticks;
// that's the ticks (from DateTime.MinValue) until 'now' (this very moment)
long ticks = new DateTime(2012, 1, 31).ToLocalTime().Ticks;
// or
long ticks = DateTime.Now.Date.Ticks;
// that's the ticks until the beginning of today
long yearmonthticks = new DateTime(2012, 1, 1).ToLocalTime().Ticks;
// that's the ticks until the beginning of the month
// etc..., the rest is simple subtractions
Since your question doesn't specify any reason not to use the DateTime constructor, this is the best solution for what seems like your problem.
I had a use case where I couldn't use DateTime but needed Years/Months from Ticks.
I used the source behind DateTime to figure out how. To go the other way you can look at the constructor, one of which calls the following code.
private static long DateToTicks(int year, int month, int day) {
if (year >= 1 && year <= 9999 && month >= 1 && month <= 12) {
int[] days = IsLeapYear(year)? DaysToMonth366: DaysToMonth365;
if (day >= 1 && day <= days[month] - days[month - 1]) {
int y = year - 1;
int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1;
return n * TicksPerDay;
}
}
throw new ArgumentOutOfRangeException(null, Environment.GetResourceString("ArgumentOutOfRange_BadYearMonthDay"));
}
This can be found in link below, of course you will need to re-write to suit your needs and look up the constants and IsLeapYear function too.
https://referencesource.microsoft.com/#mscorlib/system/datetime.cs,602
I'm trying to determine a TimeSpan (eg., time between logout and login), but with the caveat that a part of each day shouldn't be counted (eg., don't count the time between 7PM and 7AM on a given day). Removing that time from full days is easy enough, but what's the best way to check for parts of a day (eg., user logs out at 6PM and logs back in at 8PM the same day) and other edge cases?
ETA: I've currently got a set-up that looks like this:
public class HasProductiveHours
{
//These fields should represent an hour of the day (in 24-hour format)
// in which production should start or stop.
public int startProductiveHour;
public int endProductiveHour;
//Resource type enum
public ResourceType resourceType
//How often this resource is collected, in seconds
public float harvestTime
public HasProductiveHours (int startProductiveHour, int endProductiveHour, ResourceType resourceType, int harvestTime)
{
// Other creation-time things
this.startProductiveHour = startProductiveHour;
this.endProductiveHour = endProductiveHour;
this.resourceType = resourceType;
this.harvestTime = harvestTime;
TimeSimulator.Instance.SimulateElapsedTime (this);
}
}
Instances of this class and its children are simulating production of some resource every hour or so (depending on the resource). There's a system in place to simulate elapsed time since the user's last logout.
DateTime lastLogoutTime;
public void Logout ()
{
// Other logout things
lastLogoutTime = DateTime.Now;
}
public void SimulateElapsedTime (HasProductiveHours toSimulate)
{
DateTime currentTime = DateTime.Now;
TimeSpan timeSinceLogout = currentTime - lastLogout;
int unproductiveHours = Math.Abs (toSimulate.startProductionHour - toSimulate.endProductionHour) * timeSinceLogout.Days;
timeSinceLogout.Subtract (new TimeSpan (unproductiveHours, 0, 0);
double secondsElapsed = timeSinceLogout.TotalSeconds;
int harvestsElapsed = (int)(secondsElapsed / toSimulate.harvestTime);
PlayerData.Instance.AddResource (toSimulate.resourceType, harvestsElapsed);
}
A few steps should easily fix this:
Check the logout time - see if it is between 7am and 7pm (The hour method gives you the number of hours since midnight. You want to see if it is >=7 and <=19). If it is not, move the time forward to 7am. Should be easy enough to do - If the time is <7am, change the time to 7am. If the Time is greater than 7pm, change the time to 7am the next day
Check the login time - see if it is between 7am and 7pm. If it is not, move the time back to 7pm. Should be easy enough to do - If the hour >7pm, set it to 7pm, if the hour is <7am, change the time to 7pm on the previous day
Check to make sure logout time < login time. If not, the entire time logged out was during the none counting hours, so your time span should be set to 0. If your logout time < login, just subtract them to get your timespan
This for the situation described in the comment:
If the day numbers are the now the same these are the steps I would take:
Create a new datetime with the logout outs date, and a time of 1900. Subtract the new date from the logout date - this will give you the timespan until the end of day
Move the logout date to 7am the next day. If the logout and the login are now on the same day, just subtract the logout from the login, this is the timespan for that day. If they are not the same date, repeat step 1
Add the timespans generated in step 1 and step 2 together. This gives your total time
For example: I logout Jan 1, at 16:00. I login Jan 2, at 10:00
The time I was logout on Jan 1 during tracking hours is 3hours (Jan 1, 19:00 - Jan 1, 16:00)
I move the logout time to Jan 2, at 07:00.
The day of logout is 2, and the day of login is 2, now I can subtract them
The time I was logout in Jan 2 during tracking hours is 3hours (Jan 2, 10:00 - Jan 2, 07:00)
I now add them together and get 6hours of logout time during tracking hours.
#thorkia has some good suggestions.
Alternatively you can calculate the time difference and then sum up (from another table) the total non-production/non-time that was covered and subtract that from the total.
So
8am-6am = 2 hours total
6am to 7am = 1 hour non-time
result = 1 hour total real-time