/*======================================================================
 FILE: icalrecur_test.c
 CREATOR: Ken Murchison 26 September 2014

 (C) COPYRIGHT 2000 Eric Busboom <eric@civicknowledge.com>

 This library is free software; you can redistribute it and/or modify
 it under the terms of either:

    The LGPL as published by the Free Software Foundation, version
    2.1, available at: https://www.gnu.org/licenses/lgpl-2.1.html

 Or:

    The Mozilla Public License Version 2.0. You may obtain a copy of
    the License at https://www.mozilla.org/MPL/
======================================================================*/

/*
 * Program for testing libical recurrence iterator on RFC 5545 examples.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <libical/ical.h>
#include <stdlib.h>

#if defined(HAVE_LIBICU)
#include <unicode/ucal.h>
#endif

struct recur
{
    const char *dtstart;
    const char *rrule;
    const char *start_at;
};

const struct recur rfc5545[] = {

    /* Every day in January, for 3 years */
    {"19980101T090000",
     "FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA",
     NULL},

    /* Yearly in June and July for 10 occurrences */
    {"19970610T090000",
     "FREQ=YEARLY;COUNT=10;BYMONTH=6,7",
     NULL},

    /* Every other year on January, February, and March for 10 occurrences */
    {"19970310T090000",
     "FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3",
     NULL},

    /* Every third year on the 1st, 100th, and 200th day for 10 occurrences */
    {"19970101T090000",
     "FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200",
     NULL},

    /* Every 20th Monday of the year */
    {"19970519T090000",
     "FREQ=YEARLY;BYDAY=20MO;COUNT=3",
     NULL},

    /* Monday of week number 20
       (where the default start of the week is Monday) */
    {"19970512T090000",
     "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;COUNT=3",
     NULL},

    /* Monday of week number 20
       (where the start of the week is Sunday) */
    {"19970512T090000",
     "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;WKST=SU;COUNT=3",
     NULL},

    /* Monday of week number 20
       (where the start of the week is Friday) */
    {"19970512T090000",
     "FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;WKST=FR;COUNT=3",
     NULL},

    /* Every Thursday in March */
    {"19970313T090000",
     "FREQ=YEARLY;BYMONTH=3;BYDAY=TH;COUNT=11",
     NULL},

    /* Every Thursday, but only during June, July, and August */
    {"19970605T090000",
     "FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;COUNT=39",
     NULL},

    /* Every 4 years, the first Thuesday after a Monday in November
       (U.S. Presidential Election Day) */
    {"19961105T090000",
     "FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;COUNT=3",
     NULL},

    /* Friday in March occurring on or after the 26th for 5 years */
    {"20060331T020000",
     "FREQ=YEARLY;UNTIL=20100326T000000Z;BYDAY=FR;BYYEARDAY=-275,-276,-277,-278,-279,-280,-281",
     NULL},

    /* Example where leap day gets skipped */
    {"20120229T120000Z",
     "FREQ=YEARLY;UNTIL=20140301T115959Z",
     NULL},

    /* Monthly on the first Friday for 10 occurrences */
    {"19970905T090000",
     "FREQ=MONTHLY;COUNT=10;BYDAY=1FR",
     NULL},

    /* Monthly on the first Friday until December 24, 1997 */
    {"19970905T090000",
     "FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR",
     NULL},

    /* Every other month on the first and last Sunday of the month
       for 10 occurrences */
    {"19970907T090000",
     "FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU",
     NULL},

    /* Monthly on the second-to-last Monday of the month for 6 months */
    {"19970922T090000",
     "FREQ=MONTHLY;COUNT=6;BYDAY=-2MO",
     NULL},

    /* Monthly on the third-to-last day of the month */
    {"19970928T090000",
     "FREQ=MONTHLY;BYMONTHDAY=-3;COUNT=6",
     NULL},

    /* Monthly on the 2nd and 15th of the month for 10 occurrences */
    {"19970902T090000",
     "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15",
     NULL},

    /* Monthly on the first and last day of the month for 10 occurrences */
    {"19970930T090000",
     "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1",
     NULL},

    /* Every 18 months on the 10th thru 15th of the month
       for 10 occurrences */
    {"19970910T090000",
     "FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15",
     NULL},

    /* Every Thuesday, every other month for 18 occurrences */
    {"19970902T090000",
     "FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;COUNT=18",
     NULL},

    /* Every Friday the 13th */
    {"19970902T090000",
     "FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;COUNT=5",
     NULL},

    /* The first Saturday that follows the first Sunday of the month */
    {"19970913T090000",
     "FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;COUNT=10",
     NULL},

    /* The third instance into the month of one of Tuesday, Wednesday,
       or Thursday, for the next 3 months */
    {"19970904T090000",
     "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3",
     NULL},

    /* The second-to-last weekday of the month */
    {"19970929T090000",
     "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7",
     NULL},

    /* An example where an invalid date (i.e., February 30) is ignored */
    {"20070115T090000",
     "FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5",
     NULL},

    /* Another example where invalid dates are ignored */
    {"20070131T090000",
     "FREQ=MONTHLY;COUNT=12",
     NULL},

    /* Weekly for 10 occurrences */
    {"19970902T090000",
     "FREQ=WEEKLY;COUNT=10",
     NULL},

    /* Weekly until December 24, 1997 */
    {"19970902T090000",
     "FREQ=WEEKLY;UNTIL=19971224T000000Z",
     NULL},

    /* Every other week */
    {"19970902T090000",
     "FREQ=WEEKLY;INTERVAL=2;WKST=SU;COUNT=13",
     NULL},

    /* Weekly on Tuesday and Thursday for five weeks */
    {"19970902T090000",
     "FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH",
     NULL},

    /* Weekly on Tuesday and Thursday for five weeks */
    {"19970902T090000",
     "FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH",
     NULL},

    /* Weekly on Sunday every four weeks */
    {"20150322T000000",
     "FREQ=WEEKLY;INTERVAL=4;BYDAY=SU;COUNT=4",
     NULL},

    /* Every other week on Monday, Wednesday, and Friday
       until December 24, 1997, starting on Monday, September 1, 1997 */
    {"19970901T090000",
     "FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR",
     NULL},

    /* Every other week on Tuesday and Thursday, for 8 occurrences */
    {"19970902T090000",
     "FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH",
     NULL},

    /* Tuesday and Sunday every other week for 4 occurrences
       (week starts on Monday) */
    {"19970805T090000",
     "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO",
     NULL},

    /* Tuesday and Sunday every other week for 4 occurrences
       (week starts on Sunday) */
    {"19970805T090000",
     "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU",
     NULL},

    /* Weekly (with a trailing semicolon that was seen in the wild on Google Calendar) */
    {"20141006T090000",
     "FREQ=WEEKLY;INTERVAL=1;COUNT=2;BYDAY=MO;",
     NULL},

    /* Daily for 10 occurrences */
    {"19970902T090000",
     "FREQ=DAILY;COUNT=10",
     NULL},

    /* Daily until December 24, 1997 */
    {"19970902T090000",
     "FREQ=DAILY;UNTIL=19971224T000000Z",
     NULL},

    /* Every other day, 20 occurrences */
    {"19970902T090000",
     "FREQ=DAILY;INTERVAL=2;COUNT=20",
     NULL},

    /* Every 10 days, 5 occurrences */
    {"19970902T090000",
     "FREQ=DAILY;INTERVAL=10;COUNT=5",
     NULL},

    /* Every day in January, for 3 years */
    {"19980101T090000",
     "FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1",
     NULL},

    /* Every 20 minutes from 9:00 AM to 4:40 PM every day */
    {"19970902T090000",
     "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=48",
     NULL},

    /* Every 3 hours from 9:00 AM to 5:00 PM on a specific day */
    {"19970902T090000",
     "FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z",
     NULL},

    /* Every 15 minutes for 6 occurrences */
    {"19970902T090000",
     "FREQ=MINUTELY;INTERVAL=15;COUNT=6",
     NULL},

    /* Every hour and a half for 4 occurrences */
    {"19970902T090000",
     "FREQ=MINUTELY;INTERVAL=90;COUNT=4",
     NULL},

    /* Every 20 minutes from 9:00 AM to 4:40 PM every day */
    {"19970902T090000",
     "FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=48",
     NULL},

    /* Every week on Thursday and Sundays (35 times in total) */
    {"20111120T100000Z",
     "FREQ=WEEKLY;COUNT=35;BYDAY=SU,TH",
     NULL},

    /* github issue180 */
    {"20141030T000000Z",
     "FREQ=DAILY;UNTIL=20141206T000000Z;BYMONTH=11,12,1,2,3,4,10",
     NULL},

    /* CalendarServer test: Specific in first 2 weeks of year */
    {"20130101T000000",
     "FREQ=YEARLY;BYWEEKNO=1,2;UNTIL=20170101T000000Z",
     NULL},

    /* CalendarServer test: Last day of year in leap year */
    {"20121231T120000",
     "FREQ=YEARLY;BYYEARDAY=366;UNTIL=20200101T000000",
     NULL},

    /* CalendarServer test: Last Friday in October */
    {"20101029T120000",
     "FREQ=YEARLY;BYDAY=-1FR;BYMONTH=10;UNTIL=20150101T000000Z",
     NULL},

    /* CalendarServer test: First Friday in April */
    {"20100402T120000",
     "FREQ=YEARLY;BYDAY=1FR;BYMONTH=4;UNTIL=20150101T000000Z",
     NULL},

    /* Test monthly with largest day of month */
    {"20150131T000000Z",
     "FREQ=MONTHLY;BYMONTHDAY=31;COUNT=12",
     NULL},

    /* Test monthly with -largest day of month */
    {"20150101T000000Z",
     "FREQ=MONTHLY;BYMONTHDAY=-31;COUNT=12",
     NULL},

    /* Test yearly with largest day of year */
    {"20121231T120000",
     "FREQ=YEARLY;BYYEARDAY=366;COUNT=3",
     NULL},

    /* Test yearly with -largest day of year */
    {"20120101T120000",
     "FREQ=YEARLY;BYYEARDAY=-366;COUNT=3",
     NULL},

    /* Every Wednesday in February, every other year, for 5 years,
       starting at January 1, 2020 */
    {"20170201T090000",
     "FREQ=YEARLY;INTERVAL=2;UNTIL=20211231T140000Z;BYMONTH=2;BYDAY=WE",
     "20200101T100000"},

    /* Monthly on the first Friday until December 29, 2017,
       starting September 15, 2017 */
    {"20170901T090000",
     "FREQ=MONTHLY;UNTIL=20171229T000000Z;BYDAY=1FR",
     "20170915T090000"},

    /* Bi-weekly on Sunday, Tuesday, and Thursday for five weeks,
       starting last day of 2016 */
    {"20161229T090000",
     "FREQ=WEEKLY;UNTIL=20170127T000000Z;WKST=MO;BYDAY=SU,TU,TH;INTERVAL=2",
     "20161231T090000"},

    /* Bi-weekly on Sunday, Tuesday, and Thursday until January 27, 2017,
       starting second day of 2017 */
    {"20161229T090000",
     "FREQ=WEEKLY;UNTIL=20170127T000000Z;WKST=MO;BYDAY=SU,TU,TH;INTERVAL=2",
     "20170102T090000"},

    /* Every 3rd day in January, starting on the 10th */
    {"20170101T090000",
     "FREQ=DAILY;UNTIL=20170131T140000Z;BYMONTH=1;INTERVAL=3",
     "20170103T090000"},

    /* Every 3 hours from 9:00 AM to 5:00 PM over 2 days,
       starting at 11PM on September 2 */
    {"20170902T090000",
     "FREQ=HOURLY;INTERVAL=3;UNTIL=20170903T170000Z",
     "20170902T230000"},

    /* Every 13 months, on May 12 */
    {"20100212T000000",
     "FREQ=MONTHLY;INTERVAL=13;BYMONTH=5;COUNT=3",
     NULL},

    /* github issue376 */
    {"20190101T100000",
     "FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;COUNT=4",
     NULL},

    /* First 2 and last 2 ISO weeks of the year on Tue */
    {"20130101T000000",
     "FREQ=YEARLY;BYWEEKNO=1,2,-1,-2;BYDAY=TU;UNTIL=20170101T000000Z",
     NULL},

    /* 53rd ISO week of the year on Tue and Sat */
    {"20130101T000000",
     "FREQ=YEARLY;BYWEEKNO=53;BYDAY=TU,SA;UNTIL=20170101T000000Z",
     NULL},

    {NULL, NULL, NULL}
};

const struct recur rscale[] = {

    /* Bad RSCALE */
    {"20131025",
     "RSCALE=RUSSIAN;FREQ=YEARLY",
     NULL},

#if defined(HAVE_LIBICU)
    /* Ethiopic last day of year */
    {"20140910",
     "RSCALE=ETHIOPIC;FREQ=YEARLY;BYMONTH=13;BYMONTHDAY=-1;COUNT=6",
     NULL},

    /* Chinese New Year */
    {"20130210",
     "RSCALE=CHINESE;FREQ=YEARLY;UNTIL=20180101",
     NULL},

    /* Chinese monthly */
    {"20140920",
     "RSCALE=CHINESE;FREQ=MONTHLY;COUNT=4",
     NULL},

    /* Islamic monthly */
    {"20131025",
     "RSCALE=ISLAMIC-CIVIL;FREQ=MONTHLY;COUNT=4",
     NULL},

    /* Islamic Ramadan */
    {"20130709",
     "RSCALE=ISLAMIC-CIVIL;FREQ=YEARLY;BYMONTH=9;COUNT=5",
     NULL},
#if defined(HAVE_ICU_DANGI)
    /* Korean Buddha birthday */
    {"20131025",
     "RSCALE=DANGI;FREQ=YEARLY;BYMONTHDAY=8;BYMONTH=4;UNTIL=20160101",
     NULL},
#endif
    /* Chinese leap month (no leap) */
    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=9;COUNT=3",
     NULL},

    /* Chinese leap month (omit) */
    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=9L;SKIP=OMIT;COUNT=2",
     NULL},

    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=4L;SKIP=OMIT;UNTIL=21000101",
     NULL},

    /* Chinese leap month (skip forward) */
    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=9L;SKIP=FORWARD;COUNT=3",
     NULL},

    /* Chinese leap month (skip back) */
    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=9L;SKIP=BACKWARD;COUNT=3",
     NULL},

    /* Chinese leap month (both) */
    {"20131025",
     "RSCALE=CHINESE;FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=9,9L;SKIP=BACKWARD;COUNT=4",
     NULL},

    /* Every other Chinese New Year, starting at Jan 1, 2016 */
    {"20130210",
     "RSCALE=CHINESE;FREQ=YEARLY;UNTIL=20200101;INTERVAL=2",
     "20160101"},

    /* Jewish leap month (omit) */
    {"20140205",
     "RSCALE=HEBREW;FREQ=YEARLY;COUNT=4",
     NULL},

    /* Jewish leap month (skip forward) */
    {"20140205",
     "RSCALE=HEBREW;FREQ=YEARLY;SKIP=FORWARD;COUNT=4",
     NULL},

    /* Jewish leap month (skip forward by month) */
    {"20140208",
     "RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=8;SKIP=FORWARD;COUNT=5",
     NULL},

    /* 30th day of Jewish leap month (skip forward) */
    {"20140302",
     "RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=30;SKIP=FORWARD;COUNT=5",
     NULL},

    /* Last day of Jewish leap month (skip forward) */
    {"20140302",
     "RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=-1;SKIP=FORWARD;COUNT=5",
     NULL},

    /* 30th day of Jewish leap month (skip backward) */
    {"20140302",
     "RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=30;SKIP=BACKWARD;COUNT=5",
     NULL},

    /* Last day of Jewish leap month (skip backward) */
    {"20140302",
     "RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=-1;SKIP=BACKWARD;COUNT=5",
     NULL},
#endif /* HAVE_LIBICU */

    /* Gregorian leap day (omit) */
    {"20120229",
     "RSCALE=GREGORIAN;FREQ=YEARLY;COUNT=4",
     NULL},

    /* Gregorian (skip back) */
    {"20140131",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;SKIP=BACKWARD;COUNT=4",
     NULL},

    /* Gregorian (skip forward) */
    {"20140131",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;SKIP=FORWARD;COUNT=4",
     NULL},

    /* Gregorian (skip forward both) */
    {"20150201",
     "RSCALE=GREGORIAN;FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=28,29;SKIP=FORWARD;COUNT=5",
     NULL},

    /* Gregorian (skip forward interval) */
    {"20140131",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;INTERVAL=3;SKIP=FORWARD;COUNT=4",
     NULL},

    /* Test monthly with largest day of month */
    {"20150131T000000Z",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;BYMONTHDAY=31;COUNT=12;SKIP=FORWARD",
     NULL},

    /* Test monthly with -largest day of month */
    {"20150101T000000Z",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;BYMONTHDAY=-31;COUNT=12;SKIP=FORWARD",
     NULL},

    /* Test monthly with largest day of month */
    {"20150131T000000Z",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;BYMONTHDAY=31;COUNT=12;SKIP=BACKWARD",
     NULL},

    /* Test monthly with -largest day of month */
    {"20150101T000000Z",
     "RSCALE=GREGORIAN;FREQ=MONTHLY;BYMONTHDAY=-31;COUNT=12;SKIP=BACKWARD",
     NULL},

    /* Test yearly with largest day of year */
    {"20121231T120000",
     "RSCALE=GREGORIAN;FREQ=YEARLY;BYYEARDAY=366;SKIP=FORWARD;COUNT=9",
     NULL},

    /* Test yearly with largest day of year */
    {"20121231T120000",
     "RSCALE=GREGORIAN;FREQ=YEARLY;BYYEARDAY=366;SKIP=BACKWARD;COUNT=9",
     NULL},

    /* Test yearly with -largest day of year */
    {"20120101T120000",
     "RSCALE=GREGORIAN;FREQ=YEARLY;BYYEARDAY=-366;SKIP=FORWARD;COUNT=9",
     NULL},

    /* Test yearly with -largest day of year */
    {"20120101T120000",
     "RSCALE=GREGORIAN;FREQ=YEARLY;BYYEARDAY=-366;SKIP=BACKWARD;COUNT=9",
     NULL},

    {NULL, NULL, NULL}
};

int main(int argc, char *argv[])
{
    /* Default to RFC 5545 tests */
    const struct recur *r = rfc5545;
    FILE *fp = fopen("test.out", "w");
    int i;
    int verbose = 0;

    if (fp == NULL) {
        fprintf(stderr, "unable to open the output file test.out\n");
        return (1);
    }

    /* Do not use getopt for command line parsing -- for portability on Windows */
    for (i = 1; i < argc; ++i) {
        if (strncmp(argv[i], "-r", 2) == 0) { /* Do RSCALE tests */
            r = rscale;
            continue;
        }

        if (strncmp(argv[i], "-v", 2) == 0) { /* Verbose output to stdout */
            verbose = 1;
            continue;
        }

        fprintf(stderr, "usage: %s [-r]\n", argv[0]);
        fclose(fp);
        return (1);
    }

    for (; r->dtstart; r++) {
        struct icalrecurrencetype rrule;
        struct icaltimetype dtstart, next;
        icalrecur_iterator *ritr;
        const char *sep = "";

        fprintf(fp, "\nRRULE:%s\n", r->rrule);
        fprintf(fp, "DTSTART:%s\n", r->dtstart);
        if (r->start_at) fprintf(fp, "START-AT:%s\n", r->start_at);
        fprintf(fp, "INSTANCES:");

        if (verbose) {
            printf("Processing %s\n", r->rrule);
        }

        dtstart = icaltime_from_string(r->dtstart);
        rrule = icalrecurrencetype_from_string(r->rrule);
        ritr = icalrecur_iterator_new(rrule, dtstart);

        if (!ritr) {
            fprintf(fp, " *** %s\n", icalerror_strerror(icalerrno));
        } else {
            if (r->start_at) {
                struct icaltimetype start = icaltime_from_string(r->start_at);
                icalrecur_iterator_set_start(ritr, start);
            }

            for (next = icalrecur_iterator_next(ritr);
                 !icaltime_is_null_time(next);
                 next = icalrecur_iterator_next(ritr)) {

                fprintf(fp, "%s%s", sep, icaltime_as_ical_string(next));
                sep = ",";
            }
            fprintf(fp, "\n");
        }

        icalrecur_iterator_free(ritr);
        free(rrule.rscale);
    }
    fclose(fp);

    return (0);
}
