an iCal processing library

iCal.cpp

Committer:
WiredHome
Date:
2017-10-01
Revision:
12:3dc52826343f
Parent:
10:deeaec151283

File content as of revision 12:3dc52826343f:

//
// TODO
//  Repeat parsing is quite weak right now. It handles repeating days w/in a week
//      (e.g. repeat weekly, on Mon, Tue, Thu)
//
//#include "stdafx.h"
//#pragma warning (disable: 4996)

#include "iCal.h"
#include <algorithm>

//#ifdef _DEBUG
//#define DEBUG "iCal"
//#endif
#ifdef WIN32
#define LF "\n"
#else // mbed
#define LF "\r\n"
#endif // WIN32
#include <cstdio>
#if (defined(DEBUG) && !defined(TARGET_LPC11U24))
#define DBG(x, ...)  std::printf("[DBG %s %3d] " x LF, DEBUG, __LINE__, ##__VA_ARGS__)
#define WARN(x, ...) std::printf("[WRN %s %3d] " x LF, DEBUG, __LINE__, ##__VA_ARGS__)
#define ERR(x, ...)  std::printf("[ERR %s %3d] " x LF, DEBUG, __LINE__, ##__VA_ARGS__)
#define INFO(x, ...) std::printf("[INF %s %3d] " x LF, DEBUG, __LINE__, ##__VA_ARGS__)
#else
#define DBG(x, ...)
#define WARN(x, ...)
#define ERR(x, ...)
#define INFO(x, ...)
#endif

Event_T EventList[EVENT_COUNT];
int EventCount = 0;
int32_t tzoTZIDSec = 0;

static const char * RPT_DAYS[] = { "SU", "MO", "TU", "WE", "TH", "FR", "SA", "" };

int GetNumEvents(void)
{
    return EventCount;
}

const char * RepeatDayAbbrev(int i)
{
    if (i < 7)
        return RPT_DAYS[i];
    else
        return RPT_DAYS[7];
}

void SortEvents()
{
    bool swapped;
    int e;
    Event_T Event;

    do  {
        swapped = false;
        for (e=0; e<EventCount-1; e++) {
            if (EventList[e].Start > EventList[e+1].Start) {
                Event = EventList[e];
                EventList[e] = EventList[e+1];
                EventList[e+1] = Event;
                swapped = true;
            }
        }
    } while (swapped);
}

uint16_t AtoIxN(char * p, int n)
{
    uint16_t res = 0;

    while (n--) {
        res = (res * 10) + (*p - '0');
        p++;
    }
    return res;
}

// YYYYMMDD[THHMMSS[Z]]
// VALUE=DATE:YYYYMMDD
time_t ParseDateStamp(char * string, tz_sec_t tzoSec)
{
    time_t tStamp;
    struct tm t;
    struct tm * tnow;

    time(&tStamp);
    tnow = localtime(&tStamp);
    if (strncmp(string, "VALUE=DATE:", 11) == 0) {
        string += 11;
    }
    //INFO("ParseDateStamp(%s,%d)\n", string, tzoSec);
    t.tm_year = AtoIxN(string, 4) - 1900;
    t.tm_mon  = AtoIxN(string+4, 2) - 1;
    t.tm_mday = AtoIxN(string+6, 2);
    if (strlen(string) > 8) {
        t.tm_hour = AtoIxN(string+9, 2);
        t.tm_min  = AtoIxN(string+11, 2);
        t.tm_sec  = AtoIxN(string+13, 2);
        t.tm_isdst = tnow->tm_isdst;
    } else {
        t.tm_hour = 0;
        t.tm_min = 0;
        t.tm_sec = 0;
        t.tm_isdst = tnow->tm_isdst;
    }
    tStamp = mktime(&t);
    if (string[strlen(string)-1] == 'Z') {
        //INFO("Applying tzoSec %d", tzoSec);
        tStamp = tStamp + tzoTZIDSec;
    } else {
        tStamp = tStamp + tzoSec;
    }
    return tStamp;
}

char * FormatCTime(time_t t)
{
    static char temp[4][80];
    static int i = 0;

    i &= 3;
    strcpy(temp[i], ctime(&t));
    temp[i][strlen(temp[i])-1] = '\0';
    return temp[i++];
}


void ShowEventInfo(Event_T & Event)
{
    char scratch[80];

    printf("*******    Summary: %s" LF, Event.Summary);
    printf("          Location: %s" LF, Event.Location);
    printf("          Category: %s" LF, Event.Category);
    printf("          Priority: %d" LF, Event.Priority);
    sprintf(scratch, "%lu ", Event.Start);
    sprintf(scratch + strlen(scratch), "%s", (Event.Start == 0) ? "" : ctime(&Event.Start));
    scratch[strlen(scratch)-1] = '\0';
    printf("             Start: %s" LF, scratch);
    sprintf(scratch, "%lu ", Event.End);
    sprintf(scratch + strlen(scratch), "%s", (Event.End == 0) ? "" : ctime(&Event.End));
    scratch[strlen(scratch)-1] = '\0';
    printf("               End: %s" LF, scratch);
    printf("             Count: %d" LF,  Event.Count);
    printf("          Interval: %d" LF, Event.Interval);
    printf("         RepeatFrq: %d" LF,  Event.RepeatFreq);
    printf("         RepeatDay: %02X" LF,  Event.RepeatDays);
    printf("    RepeatMonthDay: %08X" LF,  Event.RepeatMonthDay);
    printf(" RepeatMonthDayRev: %08X" LF, Event.RepeatMonthDayRev);
    printf("       RepeatMonth: %04X" LF,  Event.RepeatMonths);
    sprintf(scratch, "%lu ", Event.Until);
    sprintf(scratch + strlen(scratch), "%s", (Event.Until == 0) ? "" : ctime(&Event.Until));
    scratch[strlen(scratch)-1] = '\0';
    printf("             Until: %s" LF, scratch);
    printf("" LF);
}


/// Computes the intersection of time1 and time2 ranges, and modifies time1
/// range to represent the intersection.
///
/// start1 is input as the start of the time1 range, and is written
///     to represent the intersection of the two ranges.
/// end1 is input as the end of the time1 range and is written to
///     represent the intersection of the two ranges.
/// start2 is the start of the time2 range.
/// end2 is the end of the time2 range.
/// returns true if the ranges have an intersection, and the time1 range
///     values have been modified.
///
bool TimeIntersects(time_t * start1, time_t * end1, time_t * start2, time_t * end2)
{
    // |----Time1----|
    //                  |--Time2--|            false
    //
    //                 |----Time1----|
    //   |--Time2--|                           false
    //
    // |----Time1----|
    //       |----Time2----|
    //       |-Time1-|                         true
    //
    //       |----Time1----|
    // |----Time2----|
    //       |-Time1-|                         true
    //
    // |----Time1-------|
    //       |-Time2-|
    //       |-Time1-|                         true
    //
    // | Time1 (end1 == 0)
    // | Time2 (end2 == 0)                     true
    //
    if (*start1 == *start2 && *end1 == 0 && *end2 == 0)
        return true;
    if (*end1 < *start2 || *end2 < *start1)
        return false;
    if (max(*start1,*start2) < min(*end1,*end2)) {
        *start1 = max(*start1,*start2);
        *end1 = min(*end1,*end2);
        return true;
    } else {
        return false;
    }
}

bool isLeapYear(time_t t)
{
    int year;
    struct tm * ts;
    ts = localtime(&t);

    year = 1900 + ts->tm_year + 1;
    if ((((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0))
        return true;
    else
        return false;
}

time_t NextInterval(time_t curTime, int repeatFreq, int interval)
{
    const time_t secperday = 60*60*24;
    const int repeatFactor[] = {0, 1, 7, 30, 365};
    int delta = repeatFactor[repeatFreq];
    if (repeatFreq == 4 && isLeapYear(curTime))
        delta += 1;
    //INFO("freq %d, interval %d, delta %d", repeatFreq, interval, delta);
    return delta * interval * secperday;
}


// start1,end1 is the time range representing the visible grid
// start2,end2 is the time range of the event being tested
// Event is also the event being tested and permits testing the repeat information.
//
// If the event repeat pattern intersects with the display pattern, indicate this as "true"
//
bool RepeatMaskIntersects(time_t * start1, time_t * end1, time_t * start2, time_t * end2, Event_T * Event)
{
    bool intersects = false;
    
    //INFO("RepeatFreq: %d", Event->RepeatFreq);
    if (Event->RepeatFreq == rptfDaily) {
        //INFO("rptfDaily is not handled");
    } else if (Event->RepeatFreq == rptfWeekly) {
        struct tm * timeinfo;
        timeinfo = localtime(start1);
        uint8_t daymask = Event->RepeatDays;
        // now, check the tm_wday (0=Sunday, 1=Monday, ...) and see if we intersect with the event time
        uint8_t testmask = 1 << timeinfo->tm_wday;
        //INFO("Mask: Event mask: %02X, test mask: %02X", daymask, testmask);
        if (daymask & testmask)
            intersects = true;
        else
            intersects = false;
        //INFO("  intersects: %02X", daymask & testmask);
        return intersects;
    } else if (Event->RepeatFreq == rptfYearly) {
        //struct tm * timeinfo;
        //timeinfo = localtime(start1);
        //INFO("rptfYearly is not handled well yet");        
    }
    //INFO("Mask: no handler, returning true");
    return true;
}

bool RepeatIntersects(time_t * start1, time_t * end1, time_t * start2, time_t * end2, Event_T * Event)
{
    
    //INFO("** 1: (%s, %s)", FormatCTime(*start1), *end1 ? FormatCTime(*end1) : "");
    //INFO("   2: (%s, %s)", FormatCTime(*start2), *end2 ? FormatCTime(*end2) : "");
    if (TimeIntersects(start1, end1, start2, end2))
        return true;
    if (Event && Event->RepeatFreq) {
        //INFO("Summary: %s", Event->Summary);
        if (Event->Start < *start2 && Event->Until > *start2 ) {           // Until=....
            do {
                time_t interval = NextInterval(*start1, Event->RepeatFreq, (Event->Interval == 0) ? 1 : Event->Interval);
                *start1 = *start1 + interval;
                if (*end1)
                    *end1   = *end1   + interval;
                //INFO("** 1: (%s, %s)", FormatCTime(*start1), *end1 ? FormatCTime(*end1) : "");
                //INFO("until (%24s, %s)", " ", FormatCTime(Event->Until));
                //INFO("   2: (%s, %s)", FormatCTime(*start2), *end2 ? FormatCTime(*end2) : "");
                if (!RepeatMaskIntersects(start1, end1, start2, end2, Event)) {
                    continue;   // we're not on a repeat cycle (e.g. wrong day of the week)
                }
                if (TimeIntersects(start1, end1, start2, end2)) {
                    return true;
                }
            } while ((*end2 == 0 || *start1 < *end2) && *start1 < Event->Until);
        } else if (Event->Start < *start2 && Event->Count) {                      // Count=
            int count = Event->Count - 1;
            do {
                time_t interval = NextInterval(*start1, Event->RepeatFreq, (Event->Interval == 0) ? 1 : Event->Interval);
                *start1 = *start1 + interval;
                if (*end1)
                    *end1   = *end1   + interval;
                //INFO("** 1: (%s, %s) - %d", FormatCTime(*start1), *end1 ? FormatCTime(*end1) : "", count);
                //INFO("   2: (%s, %s)", FormatCTime(*start2), *end2 ? FormatCTime(*end2) : "");
                if (!RepeatMaskIntersects(start1, end1, start2, end2, Event)) {
                    continue;   // we're not on a repeat cycle (e.g. wrong day of the week)
                }
                if (TimeIntersects(start1, end1, start2, end2)) {
                    return true;
                }
            } while (--count);
        } else if (Event->Start < *start2) {                      // no Count= and no Until=
            do {
                int rptFreq = Event->RepeatFreq;
                if (Event->RepeatFreq == 2 && Event->RepeatDays != 0)
                    rptFreq--;
                time_t interval = NextInterval(*start1, rptFreq, (Event->Interval == 0) ? 1 : Event->Interval);
                *start1 = *start1 + interval;
                if (*end1)
                    *end1   = *end1   + interval;
                //INFO("== 1: (%s, %s)", FormatCTime(*start1), *end1 ? FormatCTime(*end1) : "");
                //INFO("   2: (%s, %s)", FormatCTime(*start2), *end2 ? FormatCTime(*end2) : "");
                if (!RepeatMaskIntersects(start1, end1, start2, end2, Event)) {
                    continue;   // we're not on a repeat cycle (e.g. wrong day of the week)
                }
                if (TimeIntersects(start1, end1, start2, end2)) {
                    return true;
                }
            } while (*start1 < *end2 || (*end2 == 0 && *start1 < *start2));
        }
    }
    //INFO("  no intersection");
    return false;
}

// All the stuff between
// BEGIN:VEVENT
// ...
// END:VEVENT
//
void ParseEvent(Event_T * Event, char * pStart, tz_sec_t tzoSec)
{
    INFO("ParseEvent(...,'%s',%d)", pStart, tzoSec);
    if (strncmp(pStart, "DTSTART:", 8) == 0) {
        Event->Start = ParseDateStamp(pStart+8, tzoSec);
        INFO("  Start: %s\n", ctime(&Event->Start));
    } else if (strncmp(pStart, "DTSTART;", 8) == 0) {
        char * p = pStart + 8;
        tzoSec = ParseTZID(p);
        p = strrchr(pStart, ':');
        if (p) {
            Event->Start = ParseDateStamp(p+1, tzoSec);
            INFO("  Start: %s\n", ctime(&Event->Start));
        }
    } else if (strncmp(pStart, "DTEND:", 6) == 0) {
        Event->End = ParseDateStamp(pStart+6, tzoSec);
        //INFO("    End: %d\n", mktime(&Event->eventEnd));
    } else if (strncmp(pStart, "DTEND;", 6) == 0) {
        char * p = pStart + 8;
        tzoSec = ParseTZID(p);
        p = strrchr(pStart, ':');
        if (p) {
            Event->End = ParseDateStamp(p+1, tzoSec);
            INFO("    End: %s\n", ctime(&Event->End));
        }
    } else if (strncmp(pStart, "SUMMARY:", 8) == 0) {
        strncpy(Event->Summary, pStart+8, SUMMARY_CHARS-1);
        Event->Summary[SUMMARY_CHARS-1] = '\0';
        //INFO(" Summary: %s\n", Event->Summary);
    } else if (strncmp(pStart, "LOCATION:", 9) == 0) {
        strncpy(Event->Location, pStart+9, LOCATION_CHARS-1);
        Event->Location[LOCATION_CHARS-1] = '\0';
        //INFO(" Location: %s\n", Event->Location);
    } else if (strncmp(pStart, "PRIORITY:", 9) == 0) {
        Event->Priority = *(pStart+9) - '0';
        //INFO("  Priority: %d\n", Event->Priority);
    } else if (strncmp(pStart, "CATEGORIES:", 11) == 0) {
        strncpy(Event->Category, pStart+11, CATEGORY_CHARS-1);
        Event->Category[CATEGORY_CHARS-1] = '\0';
        //INFO(" Category: %s\n", Event->Category);
    } else if (strncmp(pStart, "RRULE:", 6) == 0) {
        //RRULE:FREQ=WEEKLY;UNTIL=20140502T180000;BYDAY=MO,TU,WE,TH,FR
        char * p1, *p2;
        //INFO("%s", pStart);
        p1 = pStart + 6;                        // p1 = FREQ=WEEKLY;UNTIL=20140502T180000;BYDAY=MO,TU,WE,TH,FR
        p2 = strchr(p1, ';');
        if (p2)
            *p2++ = '\0';
        while (*p1) {
            //INFO("%s", p1);
            if (strncmp(p1, "FREQ=", 5) == 0) {
                //INFO("%s", p1);
                p1 += 5;                            // p1 = WEEKLY;UNTIL=20140502T180000;BYDAY=MO,TU,WE,TH,FR
                if (strncmp(p1, "WEEKLY", 6) == 0) {
                    //INFO("  %s", p1);
                    Event->RepeatFreq = rptfWeekly;
                    p1 += 6;
                } else if (strncmp(p1, "DAILY", 5) == 0) {
                    //INFO("  %s", p1);
                    Event->RepeatFreq = rptfDaily;
                    p1 += 5;
                } else if (strncmp(p1, "MONTHLY", 7) == 0) {
                    //INFO("  %s", p1);
                    Event->RepeatFreq = rptfMonthly;
                    p1 += 7;
                } else if (strncmp(p1, "YEARLY", 6) == 0) {
                    //INFO("  %s", p1);
                    Event->RepeatFreq = rptfYearly;
                    p1 += 6;
                }
            } else if (strncmp(p1, "INTERVAL=", 9) == 0) { // INTERVAL=2
                //INFO("%s", p1);
                p1 += 9;
                Event->Interval = atoi(p1);
            } else if (strncmp(p1, "COUNT=", 6) == 0) { // COUNT=12;
                //INFO("%s", p1);
                p1 += 6;                // p1 =
                Event->Count = atoi(p1);
            } else if (strncmp(p1, "UNTIL=", 6) == 0) {
                //INFO("%s", p1);
                p1 += 6;                // p1 = 20140502T180000;BYDAY=MO,TU,WE,TH,FR
                //printf("UNTIL= {%s}\n", p1);
                Event->Until = ParseDateStamp(p1, tzoSec);
                //printf("UNTIL:: %d: %d\n", Event->Until, tzoSec);
            } else if (strncmp(p1, "BYDAY=", 6) == 0) {
                //INFO("%s", p1);
                p1 += 6;        // p1 = MO,TU,WE,TH,FR
                while (*p1 >= ' ') {
                    //INFO("  %s", p1);
                    for (int d=0; d<7; d++) {
                        if (strncmp(p1,RepeatDayAbbrev(d),2) == 0) {
                            Event->RepeatDays |= (1 << d);
                            //INFO("    %s %02X", RepeatDayAbbrev(d), Event->RepeatDays);
                            break;
                        }
                    }
                    p1 += 3;
                }
                //INFO("  RepeatDay: %02X", Event->RepeatDays);
            } else if (strncmp(p1, "BYMONTHDAY=", 11) == 0) {
                // RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15
                p1 += 11;
                while (*p1 >= ' ') {
                    char * px = p1;
                    while (*px >= ' ' && *px != ',') {    // find , or ; or <nul>
                        px++;
                    }
                    if (*px)
                        *px++ = '\0';
                    int num = atoi(p1);
                    if (num >= 0)
                        Event->RepeatMonthDay |= (1 << num);
                    else
                        Event->RepeatMonthDayRev |= (1 << -num);
                    p1 = px;
                }
                INFO("  RepeatMonthDay: %08X", Event->RepeatMonthDay);
            } else if (strncmp(p1, "BYMONTH=", 8) == 0) {
                // RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3
                p1 += 8;
                while (*p1 >= ' ') {
                    char * px = p1;
                    while (*px >= ' ' && *px != ',') {    // find , or ; or <nul>
                        px++;
                    }
                    if (*px)
                        *px++ = '\0';
                    int num = atoi(p1);
                    if (num >= 0)
                        Event->RepeatMonths |= (1 << num);
                    //else
                    //    ; // Event->RepeatMonthsRev |= (1 << -num);
                    p1 = px;
                }
                INFO("  RepeatMonths: %04X", Event->RepeatMonths);                
            }
            if (!p2)
                break;
            p1 = p2;
            p2 = strchr(p1, ';');
            if (p2)
                *p2++ = '\0';
        }
    }
}


// TZID="(GMT -06:00)":20140519T063000
// TZID:(UTC-06:00) Central Time (US & Canada)
// TZID:(GMT -06:00)
tz_sec_t ParseTZID(char * string)
{
    tz_sec_t tzo = 0;
    bool sign = false;

    INFO("ParseTZID(%s)", string);
    if (*string == '"')     // TZID="(GMT -06:00)":20140519T063000 
        string++;
    if ((strncmp(string, "(UTC", 4) == 0) 
    ||  (strncmp(string, "(GMT", 4) == 0) ){
        string += 4;
        if (*string == ' ')
            string++;
        if (*string == '-') {
            sign = true;
            string++;
        }
        tzo = atoi(string) * 3600;
        string = strchr(string, ':');
        if (string) {
            string++;
            tzo += atoi(string) * 60;
        }
        if (sign)
            tzo = -tzo;
        INFO("  tzo = %d", tzo);
    } else {
        ERR("Unhandled TZID(%s)", string);
    }
    return tzo;
}

int ParseICalStream(char * pStart, time_t gridStartTime, time_t gridEndTime, tz_min_t tzoMin, bool showEvents)
{
    Event_T Event;
    bool tzAdjusted = false;
    char * pEnd;
    typedef enum { idle, inTimeZone, inEvent } seekstate_t;
    seekstate_t seeking = idle;

    EventCount = 0;
    if (gridEndTime == gridStartTime)
        gridEndTime++;  // advance so they are not equal
    while (pStart && EventCount < EVENT_COUNT) {
        pEnd = strchr(pStart, '\n');
        if (pEnd) {
            if (*(pEnd-1) == '\r')
                pEnd--;
            *pEnd++ = '\0';
            while (*pEnd && *pEnd < ' ') {
                pEnd++;
            }
            // pStart now has a single null terminated line of text.
            if (showEvents)
                INFO("*** %s", pStart);
            switch (seeking) {
                case idle:
                    if (strcmp(pStart, "BEGIN:VTIMEZONE") == 0)
                        seeking = inTimeZone;
                    else if (strcmp(pStart, "BEGIN:VEVENT") == 0) {
                        seeking = inEvent;
                        Event.Start = 0;
                        Event.End = 0;
                        Event.Until = 0;
                        Event.Summary[0] = '\0';
                        Event.Location[0] = '\0';
                        Event.Category[0] = '\0';
                        Event.Count = 0;
                        Event.Interval = 0;
                        Event.RepeatFreq = rptfNone;
                        Event.RepeatDays = 0;
                        Event.RepeatMonthDay = 0;
                        Event.RepeatMonthDayRev = 0;
                        Event.RepeatMonths = 0;
                        Event.Priority = 5;     // 5 is Normal
                    }
                    break;
                case inTimeZone:
                    // Can also pick up daylight savings time
                    if (strcmp(pStart, "END:VTIMEZONE") == 0) {
                        seeking = idle;
                    } else if ((strncmp(pStart, "TZID:", 5) == 0) 
                    || (strncmp(pStart, "TZID=", 5) == 0) ) {
                        tzoTZIDSec = ParseTZID(pStart + 5);
                        tzAdjusted = true;
                    } else if (strncmp(pStart, "BEGIN:STANDARD", 14) == 0) {
                        
                    } else if (strncmp(pStart, "BEGIN:DAYLIGHT", 14) == 0) {
                        
                    }
                    break;
                case inEvent:
                    // inEvent
                    if (strcmp(pStart, "END:VEVENT") == 0) {
                        // Timezone offset
                        if (!tzAdjusted) {
                            Event.Start += (60 * tzoMin);
                            if (Event.End)
                                Event.End += (60 * tzoMin);
                        }
                        // Process it
                        if (showEvents)
                            ShowEventInfo(Event);
                        // Force to ALWAYS
                        if (1 || (Event.Start >= gridStartTime && Event.Start < gridEndTime)
                                || (Event.End >= gridStartTime && Event.End < gridEndTime)) {
                            EventList[EventCount++] = Event;
                            if (showEvents) {
                                INFO(" +++++ Added Event %d: %s", EventCount, Event.Summary);
                            }
                        }
                        seeking = idle;
                    } else {
                        ParseEvent(&Event, pStart, 60 * tzoMin);
                    }
                    // End of inEvent
                    break;
                default:
                    seeking = idle;
                    break;
            }
            pStart = pEnd;
        } else {
            pStart = NULL;
        }
    } // while
    if (EventCount > 0)
        SortEvents();
    return GetNumEvents();
}