leora
leora

Reputation: 196459

How to handle daylight saving when using the Outlook C# GetFreeBusy() API?

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?

Upvotes: 1

Views: 716

Answers (2)

Mike Cowan
Mike Cowan

Reputation: 919

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.

Upvotes: 0

user3473830
user3473830

Reputation: 7285

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:

time zone test

Upvotes: 3

Related Questions