ronnieholm / SPCalendarRecurrenceExpander

Turns each SharePoint calendar recurrence event into a series of individual events, taking into account recurrence exceptions
BSD 2-Clause "Simplified" License
14 stars 5 forks source link

toLocalDateTime does not appear to be aware of daylight time boundaries? #3

Closed Trailbear closed 1 year ago

Trailbear commented 8 years ago

Apologies if I've read the code wrong, but I think there may be an issue with daylight time during expansion.

In Expander.fs on lines 9 and 10, the code appears to use the same daylight bias value regardless of the date it is processing. I think this means that if a series has instances both during standard time and during daylight time, then some will have the incorrect bias applied.

The current bias changes as time progresses out of standard time into daylight time and vice versa. So the bias that is correct for dates in the same period is incorrect for dates in the opposite period. So typically expanded times are correct before the boundary date and incorrect after, but if you look far enough ahead you will see the times are correct again as you pass the next transition. So now, as you approach and pass into the next period of the year, this would reverse, and the times that appeared wrong before would suddenly be correct. But looking far into the future, the ones that were correct would be wrong now. So the test case needs to look at two instances, one on each side of a standard/daylight boundary, but not both in the same type of period, and ensure that the correct bias is applied such that their properties have the same time of day.

The good news... It seems you can avoid passing in the bias entirely, because SharePoint is already including it for every appointment. The property I see has the key XMLTZone and its value seems to contain all the information needed to make an informed decision about whether daylight time is active.

Here is a sample of the contents of the value for that key:

<timeZoneRule>
    <standardBias>480</standardBias>
    <additionalDaylightBias>-60</additionalDaylightBias>
    <standardDate>
        <transitionRule weekdayOfMonth="first" day="su" month="11"/>
        <transitionTime>2:0:0</transitionTime>
    </standardDate>
    <daylightDate>
        <transitionRule weekdayOfMonth="second" day="su" month="3"/>
        <transitionTime>2:0:0</transitionTime>
    </daylightDate>
</timeZoneRule>

I wonder if using System.TimeZoneInfo.CreateCustomTimeZone in conjunction with this would be an approach that could save some code? https://msdn.microsoft.com/en-us/library/bb381969(v=vs.110).aspx

I'm not sure if the XMLTZone can be different for appointments in the same list, but you could at least avoid having to pass this value in the parameters. And if you reference the property value from each master appointment as you process it you could preemptively avoid the unknown case where it is different for two series in the same list.

And thanks for the work on this project!

-Respectfully Trailbear (Frank Long)

ronnieholm commented 8 years ago

Hi Frank,

You're absolutely right. I haven't considered the case of an appointment crossing a daylight time boundary. And hadn't noticed timeZoneRole inside the list item.

Either I need to take timeZoneRule into account or convert the input date/time of an appointment to UTC. While converting to UTC seems simple, I have a feeling it's not.

Let me create a few appointments and see what timeZoneRule gets populated with. I notice the transition rule doesn't contain a year component. Don't know if that makes sense or not.

Trailbear commented 8 years ago

The rule should be used year after year and not be specific to one year. in plain language is specified as the "First Sunday of November". This is so that it is easy for the average person to remember. In this case in the US it is the day after Halloween. It used to be just before Halloween and the candy makers got it changed so that kids can wander around for another hour before it gets really dark.
Generally speaking if you can use a system library for conversion so much the better. There are boundary cases everywhere. At a minimum you have to consider midnight, month end/start and year end/start.

Now, that said, if you were doing historical dates you would need a table of when the boundaries changed year by year. Going forward, there may come a time when the rules change again. Of course that will happen at a given moment in time. So maybe that's where SharePoint will provide a different XMLTZone value per master appointment.

Unfortunately, the _predefined _timezones are pulled from each machine's registry. So you don't want to start down that route. They are identified by a free-text name, so a simple translation will leave you unable to locate anything..

But with all the info in the XMLTZone field it looks like they have provided everything needed to need to create a custom timezone. Once this is done I believe you can then use that timezone to call the ConvertTimeToUtc() or ConvertTimeFromUtc() This article is a pretty thorough treatment of the subject.. https://msdn.microsoft.com/en-us/library/bb397783(v=vs.110).aspx

ronnieholm commented 8 years ago

Closed by accident.

ronnieholm commented 8 years ago

I did some preliminary analysis to see when and what gets included with XMLTZone.

Platform: SharePoint Online Location: Denmark Time zone: UTC+1 Daylight saving time: Sun, 27 Mar to Sun, 30 Oct.


Case 1: Regular no-recurring appointment from 9pm-10pm local time:

[[("ID", Some 42); ("EventDate", Some 5/23/2016 7:00:00 PM);
  ("EndDate", Some 5/23/2016 8:00:00 PM); ("Duration", Some 3600);
  ("fRecurrence", Some false); ("EventType", Some 0);
  ("MasterSeriesItemID", Some null); ("RecurrenceID", Some null);
  ("RecurrenceData", Some null); ("fAllDayEvent", Some false);
  ("XMLTZone", Some null)]]

Times are in UTC, but no time zone information associated with item.

We'll need to get time zone info from from ctx.Web.RegionalSettings.TimeZone.


Case 2: Recurring appointment from 9pm-10pm local time:

[[("ID", Some 43); ("EventDate", Some 5/23/2016 7:00:00 PM);
  ("EndDate", Some 5/25/2016 8:00:00 PM); ("Duration", Some 3600);
  ("fRecurrence", Some true); ("EventType", Some 1);
  ("MasterSeriesItemID", Some null); ("RecurrenceID", Some null);
  ("RecurrenceData",
   Some
     "<recurrence>
          <rule>
              <firstDayOfWeek>mo</firstDayOfWeek>
              <repeat>
                  <daily dayFrequency="1" />
              </repeat>
              <repeatInstances>3</repeatInstances>
          </rule>
     </recurrence>");
  ("fAllDayEvent", Some false);
  ("XMLTZone",
   Some
     "<timeZoneRule>
        <standardBias>-60</standardBias>
        <additionalDaylightBias>-60</additionalDaylightBias>
        <standardDate>
            <transitionRule  month='10' day='su' weekdayOfMonth='last' />
            <transitionTime>3:0:0</transitionTime>
        </standardDate>
        <daylightDate>
            <transitionRule  month='3' day='su' weekdayOfMonth='last' />
            <transitionTime>2:0:0</transitionTime>
        </daylightDate>
    </timeZoneRule>")]]

Contains XMLTZone information, while EventDate/EndDate are in UTC. We'll need to use the existing function to resolve which date the last Sun of the 3rd and 10th months are.

Example: A recurrence appointment crossing October 30 going from date/time x to date/time y should maintain x to y in local time. In local time zone appointments don't change times, only relative to UTC they do.


Case 3: Recurring all day appointment

[[("ID", Some 44); ("EventDate", Some 5/23/2016 12:00:00 AM);
  ("EndDate", Some 5/25/2016 11:59:00 PM); ("Duration", Some 86340);
  ("fRecurrence", Some true); ("EventType", Some 1);
  ("MasterSeriesItemID", Some null); ("RecurrenceID", Some null);
  ("RecurrenceData",
   Some
     "<recurrence>
          <rule>
              <firstDayOfWeek>mo</firstDayOfWeek>
              <repeat>
                  <daily dayFrequency="1" />
              </repeat>
              <repeatInstances>3</repeatInstances>
          </rule>
      </recurrence>");
  ("fAllDayEvent", Some true);
  ("XMLTZone",
   Some
     "<timeZoneRule>
          <standardBias>0</standardBias>
          <additionalDaylightBias>0</additionalDaylightBias>
      </timeZoneRule>")]]

Times are in local time for whole day appointments. In principle the time component can be ignored. No timezone information present.


Case 4: Recurrence exception to whole day appointment

[[("ID", Some 44); ("EventDate", Some 5/23/2016 12:00:00 AM);
  ("EndDate", Some 5/25/2016 11:59:00 PM); ("Duration", Some 86340);
  ("fRecurrence", Some true); ("EventType", Some 1);
  ("MasterSeriesItemID", Some null); ("RecurrenceID", Some null);
  ("RecurrenceData",
   Some
     "<recurrence>
          <rule>
              <firstDayOfWeek>mo</firstDayOfWeek>
              <repeat>
                  <daily dayFrequency="1" />
              </repeat>
              <repeatInstances>3</repeatInstances>
          </rule>
      </recurrence>");
  ("fAllDayEvent", Some true);
  ("XMLTZone",
   Some
     "<timeZoneRule>
          <standardBias>0</standardBias><additionalDaylightBias>0</additionalDaylightBias></timeZoneRule>")];

 [("ID", Some 45); ("EventDate", Some 5/24/2016 1:00:00 PM);
  ("EndDate", Some 5/24/2016 3:00:00 PM); ("Duration", Some 7200);
  ("fRecurrence", Some true); ("EventType", Some 4);
  ("MasterSeriesItemID", Some 44); ("RecurrenceID", Some 5/24/2016 12:00:00 AM);
  ("RecurrenceData", Some "Hver 1. dag"); ("fAllDayEvent", Some false);
  ("XMLTZone", Some null)]]

No timezone information present. RecurrenceID must be kept in local time or the combination of MasterSeriesItemID and RecurrenceID doesn't match with master item.

ronnieholm commented 1 year ago

Closing. Probably no one is using this library anymore.