Exchange Web Services Time Zone Conversion in Exchange 2010

Last modified: May 30, 2013

Applies to: Exchange Server 2010

In this article
Converting Time Zone by Using the EWS Managed API
Converting Time Zone by Using the Exchange 2010 Proxy Classes
Conclusion

By Kim Brandl & Andrew Salamatov

Microsoft Exchange Server 2010 includes enhanced functionality for managing time zones in Exchange Web Services (EWS). Some of the main enhancements include:

  • The ability to get a list of all time zones that are supported by the Exchange server.

  • The ability to specify time zone in the EWS request, making it possible for EWS to correctly handle a recurring series of tasks that spans a time change transition.

  • The ability to represent more complex time zones structures. For example, time zones that have multiple daylight saving time changes per calendar year can be represented by EWS in Exchange 2010.

When it comes to using time zones, one of the most common development scenarios is converting date/time values from one time zone to another. For example, an EWS client that needs to render calendar items in different time zones would convert all date/time property values (that is, start time, end time, reminder due by time, and so on) to the correct time zone before rendering each calendar item. Although it is possible to represent more complex time zone structures in EWS in Exchange 2010 (certainly a good thing), a more complex time zone structure means a slightly more complex time zone conversion process. But don’t worry! This article shows you how to convert a date/time value from one time zone to another, by using either the EWS Managed API or the Exchange Web Services proxy classes in Exchange 2010.

Converting Time Zone by Using the EWS Managed API

The EWS Managed API is a fully object-oriented API that provides an intuitive Microsoft .NET Framework interface for developing client applications that use Exchange Web Services. The following code example shows how to use the EWS Managed API to get a calendar item and convert its Start date/time to Eastern Standard Time.

// Get an existing calendar item, requesting the Id, Start, and StartTimeZone properties.
PropertySet props = new PropertySet(
      AppointmentSchema.Id, 
      AppointmentSchema.Start, 
      AppointmentSchema.StartTimeZone);
Appointment appt = Appointment.Bind(service, new ItemId("AQMkA="), props);

// Examine Start property value before time zone conversion.
Console.WriteLine("appt.Start.Kind: " + appt.Start.Kind.ToString()); //Local
Console.WriteLine("Start date/time before conversion (" + appt.StartTimeZone.Id + "): " + appt.Start); // (Pacific Standard Time) 6/11/2009 4:00:00 PM 

// Convert Start date/time to Eastern Standard Time.
DateTime startEasternStandardTime = TimeZoneInfo.ConvertTime(
      appt.Start, 
      service.TimeZone, // source time zone
      TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"));

// Examine Start property value after time zone conversion.
Console.WriteLine("Start date/time after conversion (Eastern Standard Time): " + startEasternStandardTime.ToString()); // (Eastern Standard Time) 6/11/2009 7:00:00 PM

As this code example shows, using the EWS Managed API allows you to take advantage of the Microsoft .NET Framework TimeZoneInfo class to convert a date/time value from one time zone to another. Just one line of code is required to perform the time zone conversion — it can’t get much quicker and easier than that!

When you load (that is, bind to) an item by using the EWS Managed API, keep in mind that all DateTime properties for the item are expressed according to the time zone of the ExchangeService instance that was used to load the item. The value of the TimeZone property of the ExchangeService object affects the value of the Kind property of the DateTime object as shown in the following table.

ExchangeService.TimeZone

DateTime.Kind

TimeZoneInfo.Utc

DateTimeKind.Utc

TimeZoneInfo.Local

DateTimeKind.Local

Not TimeZoneInfo.Utc or TimeZoneInfo.Local

DateTimeKind.Unspecified

We recommend that you always explicitly specify both the source time zone and the destination time zone when performing time zone conversion, as this code example illustrates with its use of the ConvertTime method. Using conversion methods that do not require you to specify the source time zone (such as DateTime.ToUniversalTime and DateTime.ToLocalTime) may cause issues when converting a DateTime object of Kind = DateTimeKind.Unspecified.

Converting Time Zone by Using the Exchange 2010 Proxy Classes

So, what does it take to perform time zone conversion by using the auto-generated proxy classes (like the ones generated by the "Add Web Reference" feature in Microsoft Visual Studio)? Unfortunately, due to the complex structure of the Exchange Web Services time zone definition in Exchange 2010, converting a date/time value from one time zone to another by using the proxy classes is no simple task. Disappointing, yes, but that’s often what you get when coding with proxy classes — code that’s difficult to write, read, and maintain. But before you get too discouraged, we’ve got some good news — and it comes in the form of the following code examples.

Note

The proxy classes that are used in the following examples were generated by using the Visual Studio 2008 wsdl.exe console application. Minor differences may exist between the proxy classes that are created by using wsdl.exe and Visual Studio 2008. For more information about using EWS proxy classes, see Using Exchange Web Services Proxy Classes.

Converting a Local or UTC DateTime to Another Time Zone

The Convert method uses the Exchange Web Services auto-generated proxy classes in Exchange 2010 to convert a DateTime of Kind UTC or Local to the specified time zone. It expects a DateTime object that is of Kind UTC or Local (which it immediately converts to UTC) because the offset specified in a serialized xs:datetime string defines how the date/time relates to UTC (that is, the offset does not define how the date/time relates to another time zone). If you need to convert a DateTime of Kind Local or UTC to another time zone, simply use this method in your project and worry no more about the complexity of time zone conversion.

/// <summary>
/// This method converts a DateTime (dt) from either UTC or local time
/// to the specified time zone (TimeZoneDefinitionType).
/// </summary>
/// <param name="dt">Date/time (DateTimeKind.Local or DateTimeKind.Utc) that is to be converted.</param>
/// <param name="intoTZ">Time zone to convert the date/time into. TimeZoneDefinitionType is in the Exchange Web Services 2010 proxy classes.</param>
/// <returns>The date/time in the specified time zone.</returns>
public static DateTime Convert(DateTime dt, TimeZoneDefinitionType intoTZ)
{
    if (dt.Kind == DateTimeKind.Local)
        dt = dt.ToUniversalTime();

    if (dt.Kind != DateTimeKind.Utc)
    {
        throw new Exception("Input parameter dt must either be DateTimeKind.Local or DateTimeKind.Utc.");
    }

    ArrayOfTransitionsType validGroup = null;
    string activePeriodId = ""; 
    PeriodType activePeriod = null;
    
    //=============================================
    // Step 1: Identify the valid group.  
    //=============================================
    
    // Sample of "Transitions" to group from Mountain Standard Time
    //=======================
    // <t:Transitions>
    //  <t:Transition>
    //    <t:To Kind="Group">0</t:To>
    //  </t:Transition>
    //  <t:AbsoluteDateTransition>
    //    <t:To Kind="Group">1</t:To>
    //    <t:DateTime>2007-01-01T00:00:00</t:DateTime>
    //  </t:AbsoluteDateTransition>
    // </t:Transitions>

    // Sample of "Transitions" to group from Israel Standard Time
    ////=======================
    // <t:Transitions>
    //  <t:Transition>
    //    <t:To Kind="Group">0</t:To>
    //  </t:Transition>
    //  <t:AbsoluteDateTransition>
    //    <t:To Kind="Group">1</t:To>
    //    <t:DateTime>2005-01-01T00:00:00</t:DateTime>
    //  </t:AbsoluteDateTransition>
    //  <t:AbsoluteDateTransition>
    //    <t:To Kind="Group">2</t:To>
    //    <t:DateTime>2006-01-01T00:00:00</t:DateTime>
    //  </t:AbsoluteDateTransition>
    //  <t:AbsoluteDateTransition>
    //      ...
    //  <t:AbsoluteDateTransition>
    //    <t:To Kind="Group">19</t:To>
    //    <t:DateTime>2023-01-01T00:00:00</t:DateTime>
    //  </t:AbsoluteDateTransition>
    // </t:Transitions>

    if (intoTZ.Transitions.Items.Length == 0) 
    {
        validGroup = intoTZ.TransitionsGroups[0];
    }
    else
    {
        string validGroupTarget = "";

        // Sort all the transitions by date/time.
        SortedList<DateTime, AbsoluteDateTransitionType> transitions = new SortedList<DateTime, AbsoluteDateTransitionType>();
        foreach (TransitionType t in intoTZ.Transitions.Items)
        {
            if (t is AbsoluteDateTransitionType)
            {
                transitions.Add((t as AbsoluteDateTransitionType).DateTime, (t as AbsoluteDateTransitionType));
            }
        }

        // If there is nothing to iterate through, there must be a simple transition with just a "To".
        // Otherwise, iterate through the transitions to find the last transition such that its DateTime is less than or equal to the given date/time.
        if (transitions.Count == 0)
        {
            validGroupTarget = intoTZ.Transitions.Items[0].To.Value;
        }
        else
        {
            foreach (KeyValuePair<DateTime, AbsoluteDateTransitionType> kvpair in transitions)
            {
                if (kvpair.Key.CompareTo(dt) <= 0)
                {
                    validGroupTarget = kvpair.Value.To.Value;
                }
            }
        }

        // Iterate through AbsoluteDateTransitions. If one that applies is not found,
        // use just the "Transition" element, as it points to the very first group that applies.
        if (validGroupTarget.Equals(string.Empty))
        {
            validGroupTarget = intoTZ.Transitions.Items[0].To.Value;
        }

        // Get the valid group.
        foreach (ArrayOfTransitionsType arr in intoTZ.TransitionsGroups)
        {
            if (arr.Id.Equals(validGroupTarget))
            {
                validGroup = arr;
            }
        }
    }

    // Verify that the valid group has been identified.
    if (validGroup == null)
    {
        throw new ArgumentException(String.Format("Unable to find a valid group for DateTime: {0} within the provided definition.  Id of Timezone Definition: {1}", dt, intoTZ.Id));
    }

    //=============================================
    // Step 2: Find the valid transition in our target group.
    //=============================================
    //
    // Sample of "Transitions Groups" to Period from Israel Standard Time
    //=======================
    // <t:TransitionsGroups>
    //  <t:TransitionsGroup Id="0">
    //    <t:Transition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2004-Standard</t:To>
    //    </t:Transition>
    //  </t:TransitionsGroup>
    //  <t:TransitionsGroup Id="1">
    //    <t:RecurringDateTransition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2005-Daylight</t:To>
    //      <t:TimeOffset>PT2H</t:TimeOffset>
    //      <t:Month>4</t:Month>
    //      <t:Day>1</t:Day>
    //    </t:RecurringDateTransition>
    //    <t:RecurringDateTransition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2005-Standard</t:To>
    //      <t:TimeOffset>PT2H</t:TimeOffset>
    //      <t:Month>10</t:Month>
    //      <t:Day>9</t:Day>
    //    </t:RecurringDateTransition>
    //  </t:TransitionsGroup>
    //  <t:TransitionsGroup Id="2">
    //    <t:RecurringDateTransition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2006-Daylight</t:To>
    //      <t:TimeOffset>PT2H</t:TimeOffset>
    //      <t:Month>3</t:Month>
    //      <t:Day>31</t:Day>
    //    </t:RecurringDateTransition>
    //    <t:RecurringDateTransition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2006-Standard</t:To>
    //      <t:TimeOffset>PT2H</t:TimeOffset>
    //      <t:Month>10</t:Month>
    //      <t:Day>1</t:Day>
    //    </t:RecurringDateTransition>
    //  </t:TransitionsGroup>
    //      ...
    //  <t:TransitionsGroup Id="19">
    //    <t:Transition>
    //      <t:To Kind="Period">trule:Microsoft/Registry/IsraelStandardTime/2023-Standard</t:To>
    //    </t:Transition>
    //  </t:TransitionsGroup>
    // </t:TransitionsGroups>

    // If there is only one "transition" - that is, there are no actual transitions - this time zone has just one period.
    // For example: Name="(GMT-07:00) Arizona" Id="US Mountain Standard Time".
    if (validGroup.Items.Length == 1)
    {
        activePeriodId = validGroup.Items[0].To.Value;
    }
    else
    {
        // Sort the transitions.
        SortedList<DateTime, TransitionType> calculatedTransitionsForThisYear = new SortedList<DateTime, TransitionType>();
        foreach (TransitionType t in validGroup.Items)
        {
            if (t is RecurringDateTransitionType)
            {
                // First, create a DateTime that can be used for comparison. Use the year from input DateTime parameter (dt).
                DateTime transitionDT = new DateTime(
                    dt.Year,
                    (t as RecurringDateTransitionType).Month,
                    (t as RecurringDateTransitionType).Day);

                transitionDT = transitionDT.Add(System.Xml.XmlConvert.ToTimeSpan((t as RecurringDateTransitionType).TimeOffset));

                // Now, "sort" by adding to the list.
                calculatedTransitionsForThisYear.Add(transitionDT, t);
            }
            else if (t is RecurringDayTransitionType)
            {
                // Convert the recurring day transition (example: "first Sunday of November") into an actual date.
                DateTime transitionDT = new DateTime(dt.Year, (t as RecurringDayTransitionType).Month, 1);

                // The value of the Occurrence property indicates ordinal reference: 1=First, 2=Second, 3=Third, -1=Last.
                int occurrence = (t as RecurringDayTransitionType).Occurrence;

                // If occurrence is positive, subtract one from occurrence and multiply by seven, take the result and add it to the transitionDT.
                // This will advance transitionDT forward by one week if the ordinal reference = 2, by two weeks if the ordinal reference = 3, and so on.
                if (occurrence > 0)
                {
                    transitionDT = transitionDT.AddDays(7 * (occurrence - 1));
                }
                else
                {
                    // If occurrence is negative, increment month and then subtract one week.
                    transitionDT = transitionDT.AddMonths(1);
                    transitionDT = transitionDT.AddDays(-7);
                }

                // At this point, the 'week' reference is known. Next, identify the day.
                DayOfWeek targetDayOfWeek = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), (t as RecurringDayTransitionType).DayOfWeek);
                while (!transitionDT.DayOfWeek.Equals(targetDayOfWeek))
                {
                    transitionDT = transitionDT.AddDays(1);
                }

                // Account for the time offset of the transition.
                transitionDT = transitionDT.Add(System.Xml.XmlConvert.ToTimeSpan((t as RecurringDayTransitionType).TimeOffset));

                // Now, "sort" by adding to the list.
                calculatedTransitionsForThisYear.Add(transitionDT, t);
            }
        }

        // At this point, we can run into an issue. Assume that one transition happens on 4/1 and the other on 10/28.
        // By this algorithm, these dates will be assigned the year from input parameter dt (suppose it is 1900): 4/1/1900 and 10/28/1900.
        // So, which transition applies for 1/1/1900? In this case, it is actually the last one from the previous year,
        // that is, 10/28/1899. To account for this, we must add an extra transition (the last one), but with year value equal
        // to the previous year, so that ALL date time values for a given year have a transition that happened before it!
        // 
        // Note that this is safe because the "Transitions" processing done earlier verified that the date in question falls within this group.

        // 1. Find the last calculated transition in this group.
        DateTime lastTransition = calculatedTransitionsForThisYear.Keys[0];
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in calculatedTransitionsForThisYear)
        {
            if (kvpair.Key.CompareTo(lastTransition) > 0)
            {
                lastTransition = kvpair.Key;
            }
        }
        TransitionType lastTransitionType = calculatedTransitionsForThisYear[lastTransition];

        // 2. Subtract a year from it.
        lastTransition = new DateTime(lastTransition.Year - 1, lastTransition.Month, lastTransition.Day, lastTransition.Hour, lastTransition.Minute, lastTransition.Second, lastTransition.Kind);

        // 3. Add it to the calculated transitions collection.
        calculatedTransitionsForThisYear.Add(lastTransition, lastTransitionType);

        // Because the date/time that we are converting is in UTC, we have to convert all transitions into UTC as well.
        // We do this by looking at each time offset in the transition element and adding the corresponding bias
        // value to it. The following code iterates through all transitions, keeping track of the
        // previous period, and adds the correct bias, so that we can convert all transition times to UTC.
        SortedList<DateTime, TransitionType> transitions2 = new SortedList<DateTime, TransitionType>();
        PeriodType previous = null;
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in calculatedTransitionsForThisYear)
        {
            if (previous != null)
            {
                // Add the previous period's bias to the current time offset to convert into UTC.
                DateTime d = kvpair.Key.Add(System.Xml.XmlConvert.ToTimeSpan(previous.Bias));
                transitions2.Add(d, kvpair.Value);
            }
            else
            {
                // In the first iteration there is no previous.
                transitions2.Add(kvpair.Key, kvpair.Value);
            }

            // Find the actual period in the list of periods.
            foreach (PeriodType p in intoTZ.Periods)
            {
                if (p.Id.Equals(kvpair.Value.To.Value))
                {
                    previous = p;
                }
            }
        }

        // Now compare and find the active period ID.
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in transitions2)
        {
            if (kvpair.Key.CompareTo(dt) <= 0)
            {
                activePeriodId = kvpair.Value.To.Value;
            }
        }
    }

    //=============================================
    // Step 3: Find the actual active period in the list of periods.
    //=============================================
    foreach (PeriodType p in intoTZ.Periods)
    {
        if (p.Id.Equals(activePeriodId))
        {
            activePeriod = p;
            break;
        }
    }
    
    //=============================================
    // Step 4: Get the bias and apply the conversion. Return the converted date/time.
    //      UTC = Local + Bias
    //          or
    //      Local = UTC - Bias;
    //=============================================
    TimeSpan bias = System.Xml.XmlConvert.ToTimeSpan(activePeriod.Bias);
    DateTime tmp = dt.Subtract(bias);
    return new DateTime(tmp.Year, tmp.Month, tmp.Day, tmp.Hour, tmp.Minute, tmp.Second, DateTimeKind.Unspecified);
}

Converting an Unspecified DateTime to Another Time Zone

But what if you need to convert a DateTime of Kind Unspecified to another time zone? You must first convert the date/time from the source time zone to UTC, and then convert the UTC date/time to the destination time zone. The GetPeriod method that is shown in the following example can be used to accomplish those tasks.

public static PeriodType GetPeriod(DateTime dt, TimeZoneDefinitionType TZdef)
{
    ArrayOfTransitionsType validGroup = null;
    string activePeriodId = ""; 
    PeriodType activePeriod = null;
    
    //=============================================
    // Step 1: Identify the valid group.  
    //=============================================
    if (TZdef.Transitions.Items.Length == 0) 
    {
        validGroup = TZdef.TransitionsGroups[0];
    }
    else
    {
        string validGroupTarget = "";

        // Sort all the transitions by date/time.
        SortedList<DateTime, AbsoluteDateTransitionType> transitions = new SortedList<DateTime, AbsoluteDateTransitionType>();
        foreach (TransitionType t in TZdef.Transitions.Items)
        {
            if (t is AbsoluteDateTransitionType)
            {
                transitions.Add((t as AbsoluteDateTransitionType).DateTime, (t as AbsoluteDateTransitionType));
            }
        }

        // If there is nothing to iterate through, there must be a simple transition with just a "To".
        // Otherwise, iterate through the transitions to find the last transition such that its DateTime is less than or equal to the given date/time.
        if (transitions.Count == 0)
        {
            validGroupTarget = TZdef.Transitions.Items[0].To.Value;
        }
        else
        {
            foreach (KeyValuePair<DateTime, AbsoluteDateTransitionType> kvpair in transitions)
            {
                if (kvpair.Key.CompareTo(dt) <= 0)
                {
                    validGroupTarget = kvpair.Value.To.Value;
                }
            }
        }

        // Iterate through AbsoluteDateTransitions. If one that applies is not found,
        // use just the "Transition" element, as it points to the very first group that applies.
        if (validGroupTarget.Equals(string.Empty))
        {
            validGroupTarget = TZdef.Transitions.Items[0].To.Value;
        }

        // Get the valid group.
        foreach (ArrayOfTransitionsType arr in TZdef.TransitionsGroups)
        {
            if (arr.Id.Equals(validGroupTarget))
            {
                validGroup = arr;
            }
        }
    }

    // Verify that the valid group has been identified.
    if (validGroup == null)
    {
        throw new ArgumentException(String.Format("Unable to find a valid group for DateTime: {0} within the provided definition.  Id of Timezone Definition: {1}", dt, TZdef.Id));
    }

    //=============================================
    // Step 2: Find the valid transition in our target group.
    //=============================================

    // If there is only one "transition" - that is, there are no actual transitions - this time zone has just one period.
    // For example: Name="(GMT-07:00) Arizona" Id="US Mountain Standard Time".
    if (validGroup.Items.Length == 1)
    {
        activePeriodId = validGroup.Items[0].To.Value;
    }
    else
    {
        // Sort the transitions.
        SortedList<DateTime, TransitionType> calculatedTransitionsForThisYear = new SortedList<DateTime, TransitionType>();
        foreach (TransitionType t in validGroup.Items)
        {
            if (t is RecurringDateTransitionType)
            {
                // First, create a DateTime that can be used for comparison. Use the year from input DateTime parameter (dt).
                DateTime transitionDT = new DateTime(
                    dt.Year,
                    (t as RecurringDateTransitionType).Month,
                    (t as RecurringDateTransitionType).Day);

                transitionDT = transitionDT.Add(System.Xml.XmlConvert.ToTimeSpan((t as RecurringDateTransitionType).TimeOffset));

                // Now, "sort" by adding to the list.
                calculatedTransitionsForThisYear.Add(transitionDT, t);
            }
            else if (t is RecurringDayTransitionType)
            {
                // Convert the recurring day transition (example: "first Sunday of November") into an actual date.
                DateTime transitionDT = new DateTime(dt.Year, (t as RecurringDayTransitionType).Month, 1);

                // The value of the Occurrence property indicates ordinal reference: 1=First, 2=Second, 3=Third, -1=Last.
                int occurrence = (t as RecurringDayTransitionType).Occurrence;

                // If occurrence is positive, subtract one from occurrence and multiply by seven, take the result and add it to the transitionDT.
                // This will advance transitionDT forward by one week if the ordinal reference = 2, by two weeks if the ordinal reference = 3, and so on.
                if (occurrence > 0)
                {
                    transitionDT = transitionDT.AddDays(7 * (occurrence - 1));
                }
                else
                {
                    // If occurrence is negative, increment month and then subtract one week.
                    transitionDT = transitionDT.AddMonths(1);
                    transitionDT = transitionDT.AddDays(-7);
                }

                // At this point, the 'week' reference is known. Next, identify the day.
                DayOfWeek targetDayOfWeek = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), (t as RecurringDayTransitionType).DayOfWeek);
                while (!transitionDT.DayOfWeek.Equals(targetDayOfWeek))
                {
                    transitionDT = transitionDT.AddDays(1);
                }

                // Account for the time offset of the transition.
                transitionDT = transitionDT.Add(System.Xml.XmlConvert.ToTimeSpan((t as RecurringDayTransitionType).TimeOffset));

                // Now, "sort" by adding to the list.
                calculatedTransitionsForThisYear.Add(transitionDT, t);
            }
        }

        // At this point, we can run into an issue. Assume that one transition happens on 4/1 and the other on 10/28.
        // By this algorithm, these dates will be assigned the year from input parameter dt (suppose it is 1900): 4/1/1900 and 10/28/1900.
        // So, which transition applies for 1/1/1900? In this case, it is actually the last one from the previous year,
        // that is, 10/28/1899. To account for this, we must add an extra transition (the last one), but with year value equal
        // to the previous year, so that ALL date time values for a given year have a transition that happened before it!
        // 
        // Note that this is safe because the "Transitions" processing done earlier verified that the date in question falls within this group.

        // 1. Find the last calculated transition in this group.
        DateTime lastTransition = calculatedTransitionsForThisYear.Keys[0];
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in calculatedTransitionsForThisYear)
        {
            if (kvpair.Key.CompareTo(lastTransition) > 0)
            {
                lastTransition = kvpair.Key;
            }
        }
        TransitionType lastTransitionType = calculatedTransitionsForThisYear[lastTransition];

        // 2. Subtract a year from it.
        lastTransition = new DateTime(lastTransition.Year - 1, lastTransition.Month, lastTransition.Day, lastTransition.Hour, lastTransition.Minute, lastTransition.Second, lastTransition.Kind);

        // 3. Add it to the calculated transitions collection.
        calculatedTransitionsForThisYear.Add(lastTransition, lastTransitionType);

        // Because the date/time that we are converting is in UTC, we have to convert all transitions into UTC as well.
        // We do this by looking at each time offset in the transition element and adding the corresponding bias
        // value to it. The following code iterates through all transitions, keeping track of the
        // previous period, and adds the correct bias, so that we can convert all transition times to UTC.
        SortedList<DateTime, TransitionType> transitions2 = new SortedList<DateTime, TransitionType>();
        PeriodType previous = null;
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in calculatedTransitionsForThisYear)
        {
            if (previous != null)
            {
                // Add the previous period's bias to the current time offset to convert into UTC.
                DateTime d = kvpair.Key.Add(System.Xml.XmlConvert.ToTimeSpan(previous.Bias));
                transitions2.Add(d, kvpair.Value);
            }
            else
            {
                // In the first iteration there is no previous.
                transitions2.Add(kvpair.Key, kvpair.Value);
            }

            // Find the actual period in the list of periods.
            foreach (PeriodType p in TZdef.Periods)
            {
                if (p.Id.Equals(kvpair.Value.To.Value))
                {
                    previous = p;
                }
            }
        }

        // Now compare and find the active period ID.
        foreach (KeyValuePair<DateTime, TransitionType> kvpair in transitions2)
        {
            if (kvpair.Key.CompareTo(dt) <= 0)
            {
                activePeriodId = kvpair.Value.To.Value;
            }
        }
    }

    //=============================================
    // Step 3: Find the actual active period in the list of periods.
    //=============================================
    foreach (PeriodType p in TZdef.Periods)
    {
        if (p.Id.Equals(activePeriodId))
        {
            activePeriod = p;
            break;
        }
    }
    return activePeriod;
}

Notice that the GetPeriod method is identical to the Convert method that we explored earlier, with the following exceptions:

  • The GetPeriod method returns a PeriodType object instead of a DateTime object.

  • In the GetPeriod method, the initial logic that verifies that the DateTime.Kind is either Local or UTC is not appropriate and therefore has been removed.

  • In the GetPeriod method, Step 3, "Find the actual active period in the list of periods," returns the active period.

  • In the GetPeriod method, Step 4, "Get the bias and apply the conversion," is not necessary and therefore has been removed.

Using the Convert Method

Now, let’s take a look at how to use the Convert method to convert a DateTime of Kind Local or UTC to another time zone. The following code example shows how to convert a calendar item’s Start date/time to Eastern Standard Time after getting the time zone definition object by using the GetServerTimeZonesType. This example assumes that appt is a valid CalendarItemType object.

// Specify Start date/time as the date/time to convert.
DateTime dateTimeToConvert = appt.Start;

// Examine the date/time value before time zone conversion.
Console.WriteLine("dateTimeToConvert.Kind: " + dateTimeToConvert.Kind.ToString()); // Utc
Console.WriteLine("date/time before conversion (" + appt.TimeZone + "): " + dateTimeToConvert); //  (tzone://Microsoft/Utc) 6/11/2009 11:00:00 PM

// Get the time zone definition for Eastern Standard Time.
GetServerTimeZonesType gstzRequest = new GetServerTimeZonesType();
gstzRequest.Ids = new string[] { "Eastern Standard Time" };
gstzRequest.ReturnFullTimeZoneData = true;
gstzRequest.ReturnFullTimeZoneDataSpecified = true;
GetServerTimeZonesResponseType gstzResponse = esb.GetServerTimeZones(gstzRequest);
GetServerTimeZonesResponseMessageType responseMsg = gstzResponse.ResponseMessages.Items[0] as GetServerTimeZonesResponseMessageType;
TimeZoneDefinitionType[] timezones = responseMsg.TimeZoneDefinitions.TimeZoneDefinition;
TimeZoneDefinitionType tzdt = timezones[0];

// Convert date/time to Eastern Standard Time.
DateTime dateTimeInEasternStandardTime = Convert(dateTimeToConvert, tzdt);

// Examine the date/time value after time zone conversion.
Console.WriteLine("dateTimeInEasternStandardTime.Kind: " + dateTimeInEasternStandardTime.Kind.ToString()); // Unspecified
Console.WriteLine("date/time after conversion (" + tzdt.Id + "): " + dateTimeInEasternStandardTime.ToString()); //  (Eastern Standard Time) 6/11/2009 7:00:00 PM

Using the GetPeriod Method

Finally, let’s take a look at how to use the GetPeriod method to convert a DateTime of Kind Unspecified from one time zone to another. The following code example shows how to convert a DateTime from Pacific Standard Time to Eastern Standard Time by using the GetPeriod method.

// Specify the DateTime that will be converted to another time zone.
DateTime dtToConvert = new DateTime(2009, 06, 11, 16, 0, 0, DateTimeKind.Unspecified);

// Get the time zone definitions for Pacific Standard Time and Eastern Standard Time.
GetServerTimeZonesType gstzRequest = new GetServerTimeZonesType();
gstzRequest.Ids = new string[] { "Pacific Standard Time", "Eastern Standard Time" };
gstzRequest.ReturnFullTimeZoneData = true;
gstzRequest.ReturnFullTimeZoneDataSpecified = true;
GetServerTimeZonesResponseType gstzResponse = esb.GetServerTimeZones(gstzRequest);
GetServerTimeZonesResponseMessageType responseMsg = gstzResponse.ResponseMessages.Items[0] as GetServerTimeZonesResponseMessageType;
TimeZoneDefinitionType[] timezones = responseMsg.TimeZoneDefinitions.TimeZoneDefinition;
TimeZoneDefinitionType tzPST = timezones[0];
TimeZoneDefinitionType tzEST = timezones[1];

// Convert dtToConvert from Pacific Standard Time to UTC.
//      UTC = Local + Bias
PeriodType pt1 = GetPeriod(dtToConvert, tzPST);
TimeSpan bias1 = System.Xml.XmlConvert.ToTimeSpan(pt1.Bias);
DateTime dtUTC = dtToConvert.Add(bias1);

// Examine the DateTime value after the conversion to UTC.
Console.WriteLine("dtUTC.Kind: " + dtUTC.Kind.ToString()); // Unspecified
Console.WriteLine("date/time after conversion to UTC: " + dtUTC); // 6/11/2009 11:00:00 PM

// Convert dtUTC to Eastern Standard Time.
//      Local = UTC - Bias
PeriodType pt2 = GetPeriod(dtUTC, tzEST);
TimeSpan bias2 = System.Xml.XmlConvert.ToTimeSpan(pt2.Bias);
DateTime dtEST = dtUTC.Subtract(bias2);

// Examine the date/time property value after conversion to EST.
Console.WriteLine("dtEST.Kind: " + dtEST.Kind.ToString()); // Unspecified
Console.WriteLine("date/time after conversion to EST: " + dtEST); //  6/11/2009 7:00:00 PM

Conclusion

In this article we’ve seen how to perform time zone conversion in Exchange Web Services by using either the EWS Managed API or the auto-generated proxy classes in Exchange 2010. While we recommend that you use the EWS Managed API whenever possible, because it’s much more intuitive and requires writing significantly less code than using the auto-generated proxy classes, we do realize that some development scenarios require the use of the auto-generated proxies. If you’re using the EWS auto-generated proxies in Exchange 2010 and need to perform time zone conversion, we hope that the sample code that is provided in this article saves you some time and effort.