[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-pim
Subject:    [Kde-pim] PATCH, RFC: Korganizer and timezones (bug 68345)
From:       Shaheed <srhaque () iee ! org>
Date:       2003-12-01 20:28:09
[Download RAW message or body]

Attached is a patch to implement timezones in .ics files (bug 68345). Just as 
I finished this, I suddenly thought of why MS Exchange may be saving 
meeting .ics files with timezones and non-UTC settings: because it wants the 
meeting time to "float" with the timezone phases.

For example, a meeting at 10:00 may be intended to be at 10:00 in both normal 
and daylight phases. Its not obvious to me how to represent such a thing in 
korganizer's event model. AFAICS, there is nothing in libkcal's Incidencebase, 
Incidence or Event classes to model this case.

If I am right, it is rather late in 3.2 to fix this, so I assume that any fix 
would have to wait till after the release.

In spite of this I can now import MS Exchange calendars sent to me with the 
knowledge that it will only be wrong half the year, and then only by an hour. 
Without the patch, the import goes wrong by a "random" amount. 

Should I commit?

Thanks, Shaheed

P.S. The patch structure may seem a little odd, but I was going out of my way 
to avoid fixing typos and broken code in libical before it gets updated post 
3.2.


["timezone.diff" (text/x-diff)]

? timezone.diff
Index: icalformatimpl.cpp
===================================================================
RCS file: /home/kde/kdepim/libkcal/icalformatimpl.cpp,v
retrieving revision 1.93
diff -u -p -r1.93 icalformatimpl.cpp
--- icalformatimpl.cpp	18 Nov 2003 23:37:26 -0000	1.93
+++ icalformatimpl.cpp	1 Dec 2003 20:16:14 -0000
@@ -23,6 +23,7 @@
 #include <qstring.h>
 #include <qptrlist.h>
 #include <qfile.h>
+#include <cstdlib>
 
 #include <kdebug.h>
 #include <klocale.h>
@@ -44,6 +45,310 @@ extern "C" {
 
 using namespace KCal;
 
+namespace KCal {
+
+// This macro reads any TZID setting for an icaltime. TBD: incorporate
+// this into icalproperty_get_dtend()/icalproperty_get_dtend()?
+#define GET_TZID(p,icaltime) \
+{ \
+  icalproperty *tzp = icalproperty_get_first_parameter(p,ICAL_TZID_PARAMETER); \
+  if (tzp) { \
+    icaltime.zone = icalparameter_get_tzid(tzp); \
+  } \
+}
+
+/**
+ * Sigh. icaltime_compare() does not handle ical_null_time()s, so we must
+ * make our own.
+ */
+static int compareDateTimes(icaltimetype a, icaltimetype b)
+{
+#define COMPARE_PAIR(field) \
+if (a.field < b.field) \
+  return -1; \
+if (a.field > b.field) \
+  return +1;
+  
+  COMPARE_PAIR(year)
+  COMPARE_PAIR(month)
+  COMPARE_PAIR(day)
+  COMPARE_PAIR(hour)
+  COMPARE_PAIR(minute)
+  COMPARE_PAIR(second)
+  return 0;
+#undef COMPARE_PAIR
+}
+
+/**
+ * A TimezonePhase represents a setting within a timezone, e.g. standard or
+ * daylight savings.
+ */
+typedef struct icaltimezonephase icaltimezonephase;
+class TimezonePhase : public icaltimezonephase {
+  public:
+    /**
+     * Contructor for a timezone phase.
+     */
+    TimezonePhase(icalcomponent *c)
+    {
+      tzname = (const char *)0;
+      is_stdandard = 1;
+      mIsStandard = 1;
+      dtstart = icaltime_null_time();
+      offsetto = 0;
+      tzoffsetfrom = 0;
+      comment = (const char *)0;
+      rdate.time = icaltime_null_time();
+      rdate.period = icalperiodtype_null_period();
+      rrule = (const char *)0;
+      memset(&mRrule, sizeof(mRrule), 0);
+
+      // Now do the ical reading.  
+      icalproperty *p = icalcomponent_get_first_property(c,ICAL_ANY_PROPERTY);  
+      while (p) {
+        icalproperty_kind kind = icalproperty_isa(p);
+        switch (kind) {
+    
+          case ICAL_TZNAME_PROPERTY:
+            tzname = icalproperty_get_tzname(p);
+            break;
+    
+          case ICAL_DTSTART_PROPERTY:  
+            dtstart = icalproperty_get_dtstart(p);
+            break;
+    
+          case ICAL_TZOFFSETTO_PROPERTY:  
+            offsetto = icalproperty_get_tzoffsetto(p);
+            break;
+    
+          case ICAL_TZOFFSETFROM_PROPERTY:  
+            tzoffsetfrom = icalproperty_get_tzoffsetfrom(p);
+            break;
+    
+          case ICAL_COMMENT_PROPERTY:  
+            comment = icalproperty_get_comment(p);
+            break;
+    
+          case ICAL_RDATE_PROPERTY:  
+            rdate = icalproperty_get_rdate(p);
+            break;
+    
+          case ICAL_RRULE_PROPERTY:  
+            mRrule = icalproperty_get_rrule(p);
+            break;
+    
+          default:
+            kdDebug(5800) << "TimezonePhase::TimezonePhase(): Unknown property: " << kind
+                      << endl;
+            break;
+        }
+        p = icalcomponent_get_next_property(c,ICAL_ANY_PROPERTY);
+      }
+    }
+    
+    /**
+     * Destructor for a timezone phase.
+     */
+    ~TimezonePhase()
+    {
+    }
+
+    /**
+     * Find the nearest start time of this phase just before a given time.
+     */
+    icaltimetype nearestStart(const icaltimetype *t)
+    {
+      icaltimetype previous = icaltime_null_time();
+      
+      // If this phase was not valid at the given time, give up.
+      if (compareDateTimes(dtstart,*t) > 0) {
+        kdDebug(5800) << "TimezonePhase::nearestStart(): Phase not valid" << endl;
+        return previous;
+      }
+      
+      icalrecur_iterator* ritr;
+      ritr = icalrecur_iterator_new(mRrule,dtstart);
+      struct icaltimetype next;
+      
+      // Main loop. Find the recurrance with the latest start date before t.
+      while (next = icalrecur_iterator_next(ritr), !icaltime_is_null_time(next)) {
+        switch (compareDateTimes(next,*t)) {
+        case -1:
+          // The new result is the better one.
+          previous = next;
+          break;
+        case 0:
+          // We found a phase that starts exactly when our event starts. This 
+          // must be the best possible match.
+          return next;
+        case +1:
+          // The previous recurrance was a better match. Go with that one.
+          return previous;
+        }
+      }
+      return previous;
+    }
+    
+    // Hide the missnamed "is_stdandard" variable in the base class.
+    int mIsStandard;
+  
+    // Supplement the "rrule" in the base class.
+    icalrecurrencetype mRrule;
+};
+
+/**
+ * A Timezone.
+ */
+typedef struct icaltimezonetype icaltimezonetype;
+class Timezone : public icaltimezonetype {
+  public:
+    /**
+     * Contructor for a timezone.
+     */
+    Timezone(icalcomponent *vtimezone)
+    {
+      tzid = (const char *)0;
+      last_mod = icaltime_null_time();
+      tzurl = (const char *)0;
+      
+      // The phases list is defined to be terminated by a phase with a
+      // null name.
+      phases = (icaltimezonephase *)malloc(sizeof(*phases));
+      phases[0].tzname = (const char *)0;
+      mPhases = new QPtrList<TimezonePhase>;
+      mPhases->setAutoDelete(true);
+
+      // Now do the ical reading.  
+      icalproperty *p = icalcomponent_get_first_property(vtimezone,ICAL_ANY_PROPERTY);   
+      while (p) {
+        icalproperty_kind kind = icalproperty_isa(p);
+        switch (kind) {
+    
+          case ICAL_TZID_PROPERTY:  
+            // The timezone id is basically a unique string which is used to 
+            // identify this timezone. Note that if it begins with a "/", then it
+            // is suppsed to have some externally specified meaning, but we are
+            // just after its unique value.
+            tzid = icalproperty_get_tzid(p);
+            break;
+    
+          case ICAL_TZURL_PROPERTY:  
+            tzurl = icalproperty_get_tzurl(p);
+            break;
+    
+          default:
+            kdDebug(5800) << "Timezone::Timezone(): Unknown property: " << kind
+                      << endl;
+            break;
+        }
+        p = icalcomponent_get_next_property(vtimezone,ICAL_ANY_PROPERTY);
+      }
+      kdDebug(5800) << "---zoneId: \"" << tzid << '"' << endl;
+    
+      icalcomponent *c;
+    
+      TimezonePhase *phase;
+      
+      // Iterate through all timezones before we do anything else. That way, the
+      // information needed to interpret times in actually usefulobject is 
+      // available below.
+      c = icalcomponent_get_first_component(vtimezone,ICAL_ANY_COMPONENT);
+      while (c) {
+        icalcomponent_kind kind = icalcomponent_isa(c);
+        switch (kind) {
+    
+          case ICAL_XSTANDARD_COMPONENT:  
+            kdDebug(5800) << "---standard phase: found" << endl;
+            phase = new TimezonePhase(c);
+            phase->mIsStandard = 1;
+            mPhases->append(phase);
+            break;
+    
+          case ICAL_XDAYLIGHT_COMPONENT:  
+            kdDebug(5800) << "---daylight phase: found" << endl;
+            phase = new TimezonePhase(c);
+            phase->mIsStandard = 0;
+            mPhases->append(phase);
+            break;
+    
+          default:
+            kdDebug(5800) << "Timezone::Timezone(): Unknown component: " << kind
+                      << endl;
+            break;
+        }
+        c = icalcomponent_get_next_component(vtimezone,ICAL_ANY_COMPONENT);
+      }
+    }
+    
+    /**
+     * Destructor for a timezone.
+     */
+    ~Timezone()
+    {
+      delete mPhases;
+      free(phases);
+    }
+  
+    /**
+     * Find the nearest timezone phase just before a given time.
+     */
+    const TimezonePhase *nearestStart(const icaltimetype *t)
+    {
+      unsigned i;
+      unsigned result = 0;
+      icaltimetype previous = icaltime_null_time();
+      icaltimetype next;
+      
+      // Main loop. Find the phase with the latest start date before t.
+      for (i = 0; i < mPhases->count(); i++) {
+        next = mPhases->at(i)->nearestStart(t);
+        switch (compareDateTimes(previous,next)) {
+        case -1:
+          // The new result is the better one.
+          previous = next;
+          result = i;
+          break;
+        case 0:
+          // Two phases with the same start date. Just carry on looking.
+          break;
+        case +1:
+          // The result we already have is the better one.
+          break;
+        }
+      }  
+      return mPhases->at(result);
+    }
+    
+    /**
+     * Convert the given time to UTC.
+     */
+    void toUtc(icaltimetype *t)
+    { 
+      // Play safe.
+      if (t->is_utc)
+        return;
+          
+      const TimezonePhase *phase = nearestStart(t);
+      
+      if (phase) {
+        // Apply the offset, and mark the structure as UTC!
+        t->second -= phase->offsetto;
+        *t = icaltime_normalize(*t);
+        t->is_utc = 1;
+        //free((void *)t->zone);
+        //t->zone = (char *)0;
+      } else {
+        kdError(5800) << "Timezone::toUtc() cannot find phase " 
+            << t->zone << endl;
+      }
+    }
+    
+    // Phases we have seen.
+    QPtrList<TimezonePhase> *mPhases;
+};
+
+}
+
 const int gSecondsPerMinute = 60;
 const int gSecondsPerHour   = gSecondsPerMinute * 60;
 const int gSecondsPerDay    = gSecondsPerHour   * 24;
@@ -53,10 +358,12 @@ ICalFormatImpl::ICalFormatImpl( ICalForm
   mParent( parent ), mCalendarVersion( 0 )
 {
   mCompat = new Compat;
+  mTimezones = new QDict<class Timezone>;
 }
 
 ICalFormatImpl::~ICalFormatImpl()
 {
+  delete mTimezones;
   delete mCompat;
 }
 
@@ -107,10 +414,10 @@ icalcomponent *ICalFormatImpl::writeTodo
   if (todo->hasStartDate()) {
     icaltimetype start;
     if (todo->doesFloat()) {
-//      kdDebug(5800) << "§§ Incidence " << todo->summary() << " floats." << endl;
+//      kdDebug(5800) << " Incidence " << todo->summary() << " floats." << endl;
       start = writeICalDate(todo->dtStart().date());
     } else {
-//      kdDebug(5800) << "§§ incidence " << todo->summary() << " has time." << endl;
+//      kdDebug(5800) << " incidence " << todo->summary() << " has time." << endl;
       start = writeICalDateTime(todo->dtStart());
     }
     icalcomponent_add_property(vtodo,icalproperty_new_dtstart(start));
@@ -148,10 +455,10 @@ icalcomponent *ICalFormatImpl::writeEven
   // start time
   icaltimetype start;
   if (event->doesFloat()) {
-//    kdDebug(5800) << "§§ Incidence " << event->summary() << " floats." << endl;
+//    kdDebug(5800) << " Incidence " << event->summary() << " floats." << endl;
     start = writeICalDate(event->dtStart().date());
   } else {
-//    kdDebug(5800) << "§§ incidence " << event->summary() << " has time." << endl;
+//    kdDebug(5800) << " incidence " << event->summary() << " has time." << endl;
     start = writeICalDateTime(event->dtStart());
   }
   icalcomponent_add_property(vevent,icalproperty_new_dtstart(start));
@@ -160,11 +467,11 @@ icalcomponent *ICalFormatImpl::writeEven
     // end time
     icaltimetype end;
     if (event->doesFloat()) {
-//      kdDebug(5800) << "§§ Event " << event->summary() << " floats." << endl;
+//      kdDebug(5800) << " Event " << event->summary() << " floats." << endl;
       // +1 day because end date is non-inclusive.
       end = writeICalDate( event->dtEnd().date().addDays( 1 ) );
     } else {
-//      kdDebug(5800) << "§§ Event " << event->summary() << " has time." << endl;
+//      kdDebug(5800) << " Event " << event->summary() << " has time." << endl;
       end = writeICalDateTime(event->dtEnd());
     }
     icalcomponent_add_property(vevent,icalproperty_new_dtend(end));
@@ -240,10 +547,10 @@ icalcomponent *ICalFormatImpl::writeJour
   if (journal->dtStart().isValid()) {
     icaltimetype start;
     if (journal->doesFloat()) {
-//      kdDebug(5800) << "§§ Incidence " << event->summary() << " floats." << endl;
+//      kdDebug(5800) << " Incidence " << event->summary() << " floats." << endl;
       start = writeICalDate(journal->dtStart().date());
     } else {
-//      kdDebug(5800) << "§§ incidence " << event->summary() << " has time." << endl;
+//      kdDebug(5800) << " incidence " << event->summary() << " has time." << endl;
       start = writeICalDateTime(journal->dtStart());
     }
     icalcomponent_add_property(vjournal,icalproperty_new_dtstart(start));
@@ -781,6 +1088,21 @@ icalcomponent *ICalFormatImpl::writeAlar
   return a;
 }
 
+// Read a timezone and store it in a list where it can be accessed as needed
+// by the other readXXX() routines. Note that no writeTimezone is needed 
+// because we always store in UTC.
+void ICalFormatImpl::readTimezone(icalcomponent *vtimezone)
+{
+  Timezone *timezone = new Timezone(vtimezone);
+             
+  // Add to the dictionary. Make sure we always have quotes!
+  if (timezone->tzid[0] != '"') {
+    mTimezones->insert(QString("\"") + timezone->tzid + '"', timezone);
+  } else {
+    mTimezones->insert(timezone->tzid, timezone);
+  }
+}
+
 Todo *ICalFormatImpl::readTodo(icalcomponent *vtodo)
 {
   Todo *todo = new Todo;
@@ -865,6 +1187,7 @@ Event *ICalFormatImpl::readEvent(icalcom
 
       case ICAL_DTEND_PROPERTY:  // start date and time
         icaltime = icalproperty_get_dtend(p);
+        GET_TZID(p,icaltime);
         if (icaltime.is_date) {
           event->setFloats( true );
           // End date is non-inclusive
@@ -984,11 +1307,13 @@ FreeBusy *ICalFormatImpl::readFreeBusy(i
 
       case ICAL_DTSTART_PROPERTY:  // start date and time
         icaltime = icalproperty_get_dtstart(p);
+        GET_TZID(p,icaltime);
         freebusy->setDtStart(readICalDateTime(icaltime));
         break;
 
       case ICAL_DTEND_PROPERTY:  // start End Date and Time
         icaltime = icalproperty_get_dtend(p);
+        GET_TZID(p,icaltime);
         freebusy->setDtEnd(readICalDateTime(icaltime));
         break;
 
@@ -1169,6 +1494,7 @@ void ICalFormatImpl::readIncidence(icalc
 
       case ICAL_DTSTART_PROPERTY:  // start date and time
         icaltime = icalproperty_get_dtstart(p);
+        GET_TZID(p,icaltime);
         if (icaltime.is_date) {
           incidence->setDtStart(QDateTime(readICalDate(icaltime),QTime(0,0,0)));
           incidence->setFloats(true);
@@ -1700,8 +2026,28 @@ QDateTime ICalFormatImpl::readICalDateTi
   kdDebug(5800) << "--- H: " << t.hour << " M: " << t.minute << " S: " << t.second
             << endl;
   kdDebug(5800) << "--- isDate: " << t.is_date << endl;
+  kdDebug(5800) << "--- isUtc: " << t.is_utc << endl;
+  kdDebug(5800) << "--- zoneId: " << t.zone << endl;
 */
 
+  // First convert the time into UTC if required.
+  if ( !t.is_utc && t.zone ) {
+    Timezone *timezone;
+    
+    // Always lookup with quotes.
+    if (t.zone[0] != '"') {
+      timezone = mTimezones->find(QString("\"") + t.zone + '"');
+    } else {
+      timezone = mTimezones->find(t.zone);
+    }
+    if (timezone) {
+      timezone->toUtc(&t);
+    } else {
+      kdError(5800) << "ICalFormatImpl::readICalDateTime() cannot find timezone " 
+            << t.zone << endl;
+    }
+  }
+  
   if ( t.is_utc && mCompat->useTimeZoneShift() ) {
 //    kdDebug(5800) << "--- Converting time to zone '" << cal->timeZoneId() << "'." << endl;
     if (mParent->timeZoneId().isEmpty())
@@ -1895,6 +2241,16 @@ bool ICalFormatImpl::populate( Calendar 
 
   icalcomponent *c;
 
+  // Iterate through all timezones before we do anything else. That way, the
+  // information needed to interpret times in actually useful objects is 
+  // available below.
+  c = icalcomponent_get_first_component(calendar,ICAL_VTIMEZONE_COMPONENT);
+  while (c) {
+//    kdDebug(5800) << "----Timezone found" << endl;
+    readTimezone(c);
+    c = icalcomponent_get_next_component(calendar,ICAL_VTIMEZONE_COMPONENT);
+  }
+
   // Iterate through all todos
   c = icalcomponent_get_first_component(calendar,ICAL_VTODO_COMPONENT);
   while (c) {
Index: icalformatimpl.h
===================================================================
RCS file: /home/kde/kdepim/libkcal/icalformatimpl.h,v
retrieving revision 1.22
diff -u -p -r1.22 icalformatimpl.h
--- icalformatimpl.h	22 Jul 2003 18:56:46 -0000	1.22
+++ icalformatimpl.h	1 Dec 2003 20:16:14 -0000
@@ -86,9 +86,11 @@ class ICalFormatImpl {
     void writeCustomProperties(icalcomponent *parent,CustomProperties *);
     void readCustomProperties(icalcomponent *parent,CustomProperties *);
     void dumpIcalRecurrence(icalrecurrencetype);
+    void readTimezone(icalcomponent *vtimezone);
 
     ICalFormat *mParent;
     Calendar *mCalendar;
+    QDict<class Timezone> *mTimezones;
 
     QString mLoadedProductId;         // PRODID string loaded from calendar file
     int mCalendarVersion;             // determines backward compatibility mode on read


_______________________________________________
kde-pim mailing list
kde-pim@mail.kde.org
https://mail.kde.org/mailman/listinfo/kde-pim
kde-pim home page at http://pim.kde.org/

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic