summaryrefslogtreecommitdiffstats
path: root/libkcal/libical/vzic-1.3/vzic-output.c
diff options
context:
space:
mode:
Diffstat (limited to 'libkcal/libical/vzic-1.3/vzic-output.c')
-rw-r--r--libkcal/libical/vzic-1.3/vzic-output.c2325
1 files changed, 2325 insertions, 0 deletions
diff --git a/libkcal/libical/vzic-1.3/vzic-output.c b/libkcal/libical/vzic-1.3/vzic-output.c
new file mode 100644
index 00000000..a23ece13
--- /dev/null
+++ b/libkcal/libical/vzic-1.3/vzic-output.c
@@ -0,0 +1,2325 @@
+/*
+ * Vzic - a program to convert Olson timezone database files into VZTIMEZONE
+ * files compatible with the iCalendar specification (RFC2445).
+ *
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * Copyright (C) 2003 Damon Chaplin.
+ *
+ * Author: Damon Chaplin <damon@gnome.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/* ALGORITHM:
+ *
+ * First we expand all the Rule arrays, so that each element only represents 1
+ * year. If a Rule extends to infinity we expand it up to a few years past the
+ * maximum UNTIL year used in any of the timezones. We do this to make sure
+ * that the last of the expanded Rules (which may be infinite) is only used
+ * in the last of the time periods (i.e. the last Zone line).
+ *
+ * The Rule arrays are also sorted by the start time (FROM + IN + ON + AT).
+ * Doing all this makes it much easier to find which rules apply to which
+ * periods.
+ *
+ * For each timezone (i.e. ZoneData element), we step through each of the
+ * time periods, the ZoneLineData elements (which represent each Zone line
+ * from the Olson file.)
+ *
+ * We calculate the start & end time of the period.
+ * - For the first line the start time is -infinity.
+ * - For the last line the end time is +infinity.
+ * - The end time of each line is also the start time of the next.
+ *
+ * We create an array of time changes which occur in this period, including
+ * the one implied by the Zone line itself (though this is later taken out
+ * if it is found to be at exactly the same time as the first Rule).
+ *
+ * Now we iterate over the time changes, outputting them as STANDARD or
+ * DAYLIGHT components. We also try to merge them together into RRULEs or
+ * use RDATEs.
+ */
+
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "vzic.h"
+#include "vzic-output.h"
+
+#include "vzic-dump.h"
+
+
+/* These come from the Makefile. See the comments there. */
+char *ProductID = PRODUCT_ID;
+char *TZIDPrefix = TZID_PREFIX;
+
+/* We expand the TZIDPrefix, replacing %D with the date, in here. */
+char TZIDPrefixExpanded[1024];
+
+
+/* We only use RRULEs if there are at least MIN_RRULE_OCCURRENCES occurrences,
+ since otherwise RDATEs are more efficient. Actually, I've set this high
+ so we only use RRULEs for infinite recurrences. Since expanding RRULEs is
+ very time-consuming, this seems sensible. */
+#define MIN_RRULE_OCCURRENCES 100
+
+
+/* The year we go up to when dumping the list of timezone changes (used
+ for testing & debugging). */
+#define MAX_CHANGES_YEAR 2030
+
+/* This is the maximum year that time_t value can typically hold on 32-bit
+ systems. */
+#define MAX_TIME_T_YEAR 2037
+
+
+/* The year we use to start RRULEs. */
+#define RRULE_START_YEAR 1970
+
+/* The year we use for RDATEs. */
+#define RDATE_YEAR 1970
+
+
+static char *WeekDays[] = { "SU", "MO", "TU", "WE", "TH", "FR", "SA" };
+static int DaysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+
+char *CurrentZoneName;
+
+
+typedef struct _VzicTime VzicTime;
+struct _VzicTime
+{
+ /* Normal years, e.g. 2001. */
+ int year;
+
+ /* 0 (Jan) to 11 (Dec). */
+ int month;
+
+ /* The day, either a simple month day number, 1-31, or a rule such as
+ the last Sunday, or the first Monday on or after the 8th. */
+ DayCode day_code;
+ int day_number; /* 1 to 31. */
+ int day_weekday; /* 0 (Sun) to 6 (Sat). */
+
+ /* The time, in seconds from midnight. The code specifies whether the
+ time is a wall clock time, local standard time, or universal time. */
+ int time_seconds;
+ TimeCode time_code;
+
+ /* The offset from UTC for local standard time. */
+ int stdoff;
+
+ /* The offset from UTC for local wall clock time. If this is different to
+ stdoff then this is a DAYLIGHT component. This is TZOFFSETTO. */
+ int walloff;
+
+ /* TRUE if the time change recurs every year to infinity. */
+ gboolean is_infinite;
+
+ /* TRUE if the change has already been output. */
+ gboolean output;
+
+ /* These are the offsets of the previous VzicTime, and are used when
+ calculating the time of the change. We place them here in
+ output_zone_components() to simplify the output code. */
+ int prev_stdoff;
+ int prev_walloff;
+
+ /* The abbreviated form of the timezone name. Note that this may not be
+ unique. */
+ char *tzname;
+};
+
+
+static void expand_and_sort_rule_array (gpointer key,
+ gpointer value,
+ gpointer data);
+static int rule_sort_func (const void *arg1,
+ const void *arg2);
+static void output_zone (char *directory,
+ ZoneData *zone,
+ char *zone_name,
+ GHashTable *rule_data);
+static gboolean parse_zone_name (char *name,
+ char **directory,
+ char **subdirectory,
+ char **filename);
+static void output_zone_to_files (ZoneData *zone,
+ char *zone_name,
+ GHashTable *rule_data,
+ FILE *fp,
+ FILE *changes_fp);
+static gboolean add_rule_changes (ZoneLineData *zone_line,
+ char *zone_name,
+ GArray *changes,
+ GHashTable *rule_data,
+ VzicTime *start,
+ VzicTime *end,
+ char **start_letter_s,
+ int *save_seconds);
+static char* expand_tzname (char *zone_name,
+ char *format,
+ gboolean have_letter_s,
+ char *letter_s,
+ gboolean is_daylight);
+static int compare_times (VzicTime *time1,
+ int stdoff1,
+ int walloff1,
+ VzicTime *time2,
+ int stdoff2,
+ int walloff2);
+static gboolean times_match (VzicTime *time1,
+ int stdoff1,
+ int walloff1,
+ VzicTime *time2,
+ int stdoff2,
+ int walloff2);
+static void output_zone_components (FILE *fp,
+ char *name,
+ GArray *changes);
+static void set_previous_offsets (GArray *changes);
+static gboolean check_for_recurrence (FILE *fp,
+ GArray *changes,
+ int idx);
+static void check_for_rdates (FILE *fp,
+ GArray *changes,
+ int idx);
+static gboolean timezones_match (char *tzname1,
+ char *tzname2);
+static int output_component_start (char *buffer,
+ VzicTime *vzictime,
+ gboolean output_rdate,
+ gboolean use_same_tz_offset);
+static void output_component_end (FILE *fp,
+ VzicTime *vzictime);
+
+static void vzictime_init (VzicTime *vzictime);
+static int calculate_actual_time (VzicTime *vzictime,
+ TimeCode time_code,
+ int stdoff,
+ int walloff);
+static int calculate_wall_time (int time,
+ TimeCode time_code,
+ int stdoff,
+ int walloff,
+ int *day_offset);
+static int calculate_until_time (int time,
+ TimeCode time_code,
+ int stdoff,
+ int walloff,
+ int *year,
+ int *month,
+ int *day);
+static void fix_time_overflow (int *year,
+ int *month,
+ int *day,
+ int day_offset);
+
+static char* format_time (int year,
+ int month,
+ int day,
+ int time);
+static char* format_tz_offset (int tz_offset,
+ gboolean round_seconds);
+static gboolean output_rrule (char *rrule_buffer,
+ int month,
+ DayCode day_code,
+ int day_number,
+ int day_weekday,
+ int day_offset,
+ char *until);
+static gboolean output_rrule_2 (char *buffer,
+ int month,
+ int day_number,
+ int day_weekday);
+
+static char* format_vzictime (VzicTime *vzictime);
+
+static void dump_changes (FILE *fp,
+ char *zone_name,
+ GArray *changes);
+static void dump_change (FILE *fp,
+ char *zone_name,
+ VzicTime *vzictime,
+ int year);
+
+static void expand_tzid_prefix (void);
+
+
+void
+output_vtimezone_files (char *directory,
+ GArray *zone_data,
+ GHashTable *rule_data,
+ GHashTable *link_data,
+ int max_until_year)
+{
+ ZoneData *zone;
+ GList *links;
+ char *link_to;
+ int i;
+
+ /* Insert today's date into the TZIDs we output. */
+ expand_tzid_prefix ();
+
+ /* Expand the rule data so that each entry specifies only one year, and
+ sort it so we can easily find the rules applicable to each Zone span. */
+ g_hash_table_foreach (rule_data, expand_and_sort_rule_array,
+ GINT_TO_POINTER (max_until_year));
+
+ /* Output each timezone. */
+ for (i = 0; i < zone_data->len; i++) {
+ zone = &g_array_index (zone_data, ZoneData, i);
+ output_zone (directory, zone, zone->zone_name, rule_data);
+
+ /* Look for any links from this zone. */
+ links = g_hash_table_lookup (link_data, zone->zone_name);
+
+ while (links) {
+ link_to = links->data;
+
+ /* We ignore Links that don't have a '/' in them (things like 'EST5EDT').
+ */
+ if (strchr (link_to, '/')) {
+ output_zone (directory, zone, link_to, rule_data);
+ }
+
+ links = links->next;
+ }
+ }
+}
+
+
+static void
+expand_and_sort_rule_array (gpointer key,
+ gpointer value,
+ gpointer data)
+{
+ char *name = key;
+ GArray *rule_array = value;
+ RuleData *rule, tmp_rule;
+ int len, max_year, i, from, to, year;
+ gboolean is_infinite;
+
+ /* We expand the rule data to a year greater than any year used in a Zone
+ UNTIL value. This is so that we can easily get parts of the array to
+ use for each Zone line. */
+ max_year = GPOINTER_TO_INT (data) + 2;
+
+ /* If any of the rules apply to several years, we turn it into a single rule
+ for each year. If the Rule is infinite we go up to max_year.
+ We change the FROM field in the copies of the Rule, setting it to each
+ of the years, and set TO to FROM, except if TO was YEAR_MAXIMUM we set
+ the last TO to YEAR_MAXIMUM, so we still know the Rule is infinite. */
+ len = rule_array->len;
+ for (i = 0; i < len; i++) {
+ rule = &g_array_index (rule_array, RuleData, i);
+
+ /* None of the Rules currently use the TYPE field, but we'd better check.
+ */
+ if (rule->type) {
+ fprintf (stderr, "Rules %s has a TYPE: %s\n", name, rule->type);
+ exit (1);
+ }
+
+ if (rule->from_year != rule->to_year) {
+ from = rule->from_year;
+ to = rule->to_year;
+
+ tmp_rule = *rule;
+
+ /* Flag that this is a shallow copy so we don't free anything twice. */
+ tmp_rule.is_shallow_copy = TRUE;
+
+ /* See if it is an infinite Rule. */
+ if (to == YEAR_MAXIMUM) {
+ is_infinite = TRUE;
+ to = max_year;
+ if (from < to)
+ rule->to_year = rule->from_year;
+ } else {
+ is_infinite = FALSE;
+ }
+
+ /* Create a copy of the Rule for each year. */
+ for (year = from + 1; year <= to; year++) {
+ tmp_rule.from_year = year;
+
+ /* If the Rule is infinite, mark the last copy as infinite. */
+ if (year == to && is_infinite)
+ tmp_rule.to_year = YEAR_MAXIMUM;
+ else
+ tmp_rule.to_year = year;
+
+ g_array_append_val (rule_array, tmp_rule);
+ }
+ }
+ }
+
+ /* Now sort the rules. */
+ qsort (rule_array->data, rule_array->len, sizeof (RuleData), rule_sort_func);
+
+#if 0
+ dump_rule_array (name, rule_array, stdout);
+#endif
+}
+
+
+/* This is used to sort the rules, after the rules have all been expanded so
+ that each one is only for one year. */
+static int
+rule_sort_func (const void *arg1,
+ const void *arg2)
+{
+ RuleData *rule1, *rule2;
+ int time1_year, time1_month, time1_day;
+ int time2_year, time2_month, time2_day;
+ int month_diff, result;
+ VzicTime t1, t2;
+
+ rule1 = (RuleData*) arg1;
+ rule2 = (RuleData*) arg2;
+
+ time1_year = rule1->from_year;
+ time1_month = rule1->in_month;
+ time2_year = rule2->from_year;
+ time2_month = rule2->in_month;
+
+ /* If there is more that one month difference we don't need to calculate
+ the day or time. */
+ month_diff = (time1_year - time2_year) * 12 + time1_month - time2_month;
+
+ if (month_diff > 1)
+ return 1;
+ if (month_diff < -1)
+ return -1;
+
+ /* Now we have to calculate the day and time of the Rule start and the
+ VzicTime, using the given offsets. */
+ t1.year = time1_year;
+ t1.month = time1_month;
+ t1.day_code = rule1->on_day_code;
+ t1.day_number = rule1->on_day_number;
+ t1.day_weekday = rule1->on_day_weekday;
+ t1.time_code = rule1->at_time_code;
+ t1.time_seconds = rule1->at_time_seconds;
+
+ t2.year = time2_year;
+ t2.month = time2_month;
+ t2.day_code = rule2->on_day_code;
+ t2.day_number = rule2->on_day_number;
+ t2.day_weekday = rule2->on_day_weekday;
+ t2.time_code = rule2->at_time_code;
+ t2.time_seconds = rule2->at_time_seconds;
+
+ /* FIXME: We don't know the offsets yet, but I don't think any Rules are
+ close enough together that the offsets can make a difference. Should
+ check this. */
+ calculate_actual_time (&t1, TIME_WALL, 0, 0);
+ calculate_actual_time (&t2, TIME_WALL, 0, 0);
+
+ /* Now we can compare the entire time. */
+ if (t1.year > t2.year)
+ result = 1;
+ else if (t1.year < t2.year)
+ result = -1;
+
+ else if (t1.month > t2.month)
+ result = 1;
+ else if (t1.month < t2.month)
+ result = -1;
+
+ else if (t1.day_number > t2.day_number)
+ result = 1;
+ else if (t1.day_number < t2.day_number)
+ result = -1;
+
+ else if (t1.time_seconds > t2.time_seconds)
+ result = 1;
+ else if (t1.time_seconds < t2.time_seconds)
+ result = -1;
+
+ else {
+ printf ("WARNING: Rule dates matched.\n");
+ result = 0;
+ }
+
+ return result;
+}
+
+
+static void
+output_zone (char *directory,
+ ZoneData *zone,
+ char *zone_name,
+ GHashTable *rule_data)
+{
+ FILE *fp, *changes_fp = NULL;
+ char output_directory[PATHNAME_BUFFER_SIZE];
+ char filename[PATHNAME_BUFFER_SIZE];
+ char changes_filename[PATHNAME_BUFFER_SIZE];
+ char *zone_directory, *zone_subdirectory, *zone_filename;
+
+ /* Set a global for the zone_name, to be used only for debug messages. */
+ CurrentZoneName = zone_name;
+
+ /* Use this to only output a particular zone. */
+#if 0
+ if (strcmp (zone_name, "Atlantic/Azores"))
+ return;
+#endif
+
+#if 0
+ printf ("Outputting Zone: %s\n", zone_name);
+#endif
+
+ if (!parse_zone_name (zone_name, &zone_directory, &zone_subdirectory,
+ &zone_filename))
+ return;
+
+ if (VzicDumpZoneNamesAndCoords) {
+ VzicTimeZoneNames = g_list_prepend (VzicTimeZoneNames,
+ g_strdup (zone_name));
+ }
+
+ sprintf (output_directory, "%s/%s", directory, zone_directory);
+ ensure_directory_exists (output_directory);
+ sprintf (filename, "%s/%s.ics", output_directory, zone_filename);
+
+ if (VzicDumpChanges) {
+ sprintf (output_directory, "%s/ChangesVzic/%s", directory, zone_directory);
+ ensure_directory_exists (output_directory);
+ sprintf (changes_filename, "%s/%s", output_directory, zone_filename);
+ }
+
+ if (zone_subdirectory) {
+ sprintf (output_directory, "%s/%s/%s", directory, zone_directory,
+ zone_subdirectory);
+ ensure_directory_exists (output_directory);
+ sprintf (filename, "%s/%s.ics", output_directory, zone_filename);
+
+ if (VzicDumpChanges) {
+ sprintf (output_directory, "%s/ChangesVzic/%s/%s", directory,
+ zone_directory, zone_subdirectory);
+ ensure_directory_exists (output_directory);
+ sprintf (changes_filename, "%s/%s", output_directory, zone_filename);
+ }
+ }
+
+ /* Create the files. */
+ fp = fopen (filename, "w");
+ if (!fp) {
+ fprintf (stderr, "Couldn't create file: %s\n", filename);
+ exit (1);
+ }
+
+ if (VzicDumpChanges) {
+ changes_fp = fopen (changes_filename, "w");
+ if (!changes_fp) {
+ fprintf (stderr, "Couldn't create file: %s\n", changes_filename);
+ exit (1);
+ }
+ }
+
+ fprintf (fp, "BEGIN:VCALENDAR\nPRODID:%s\nVERSION:2.0\n", ProductID);
+
+ output_zone_to_files (zone, zone_name, rule_data, fp, changes_fp);
+
+ if (ferror (fp)) {
+ fprintf (stderr, "Error writing file: %s\n", filename);
+ exit (1);
+ }
+
+ fprintf (fp, "END:VCALENDAR\n");
+
+ fclose (fp);
+
+ g_free (zone_directory);
+ g_free (zone_subdirectory);
+ g_free (zone_filename);
+}
+
+
+/* This checks that the Zone name only uses the characters in [-+_/a-zA-Z0-9],
+ and outputs a warning if it isn't. */
+static gboolean
+parse_zone_name (char *name,
+ char **directory,
+ char **subdirectory,
+ char **filename)
+{
+ static int invalid_zone_num = 1;
+
+ char *p, ch, *first_slash_pos = NULL, *second_slash_pos = NULL;
+ gboolean invalid = FALSE;
+
+ for (p = name; (ch = *p) != 0; p++) {
+ if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')
+ && (ch < '0' || ch > '9') && ch != '/' && ch != '_'
+ && ch != '-' && ch != '+') {
+ fprintf (stderr, "WARNING: Unusual Zone name: %s\n", name);
+ invalid = TRUE;
+ break;
+ }
+
+ if (ch == '/') {
+ if (!first_slash_pos) {
+ first_slash_pos = p;
+ } else if (!second_slash_pos) {
+ second_slash_pos = p;
+ } else {
+ fprintf (stderr, "WARNING: More than 2 '/' characters in Zone name: %s\n", name);
+ invalid = TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!first_slash_pos) {
+#if 0
+ fprintf (stderr, "No '/' character in Zone name: %s. Skipping.\n", name);
+#endif
+ return FALSE;
+ }
+
+ if (invalid) {
+ *directory = g_strdup ("Invalid");
+ *filename = g_strdup_printf ("Zone%i", invalid_zone_num++);
+ } else {
+ *first_slash_pos = '\0';
+ *directory = g_strdup (name);
+ *first_slash_pos = '/';
+
+ if (second_slash_pos) {
+ *second_slash_pos = '\0';
+ *subdirectory = g_strdup (first_slash_pos + 1);
+ *second_slash_pos = '/';
+
+ *filename = g_strdup (second_slash_pos + 1);
+ } else {
+ *subdirectory = NULL;
+ *filename = g_strdup (first_slash_pos + 1);
+ }
+ }
+
+ return invalid ? FALSE : TRUE;
+}
+
+
+static void
+output_zone_to_files (ZoneData *zone,
+ char *zone_name,
+ GHashTable *rule_data,
+ FILE *fp,
+ FILE *changes_fp)
+{
+ ZoneLineData *zone_line;
+ GArray *changes;
+ int i, stdoff, walloff, start_index, save_seconds;
+ VzicTime start, end, *vzictime_start, *vzictime, *vzictime_first_rule_change;
+ gboolean is_daylight, found_letter_s;
+ char *start_letter_s;
+
+ changes = g_array_new (FALSE, FALSE, sizeof (VzicTime));
+
+ vzictime_init (&start);
+ vzictime_init (&end);
+
+ /* The first period starts at -infinity. */
+ start.year = YEAR_MINIMUM;
+
+ for (i = 0; i < zone->zone_line_data->len; i++) {
+ zone_line = &g_array_index (zone->zone_line_data, ZoneLineData, i);
+
+ /* This is the local standard time offset from GMT for this period. */
+ start.stdoff = stdoff = zone_line->stdoff_seconds;
+ start.walloff = walloff = stdoff + zone_line->save_seconds;
+
+ if (zone_line->until_set) {
+ end.year = zone_line->until_year;
+ end.month = zone_line->until_month;
+ end.day_code = zone_line->until_day_code;
+ end.day_number = zone_line->until_day_number;
+ end.day_weekday = zone_line->until_day_weekday;
+ end.time_seconds = zone_line->until_time_seconds;
+ end.time_code = zone_line->until_time_code;
+ } else {
+ /* The last period ends at +infinity. */
+ end.year = YEAR_MAXIMUM;
+ }
+
+ /* Add a time change for the start of the period. This may be removed
+ later if one of the rules expands to exactly the same time. */
+ start_index = changes->len;
+ g_array_append_val (changes, start);
+
+ /* If there are Rules associated with this period, add all the relevant
+ time changes. */
+ save_seconds = 0;
+ if (zone_line->rules)
+ found_letter_s = add_rule_changes (zone_line, zone_name, changes,
+ rule_data, &start, &end,
+ &start_letter_s, &save_seconds);
+ else
+ found_letter_s = FALSE;
+
+ /* FIXME: I'm not really sure what to do about finding a LETTER_S for the
+ first part of the period (i.e. before the first Rule comes into effect).
+ Currently we try to use the same LETTER_S as the first Rule of the
+ period which is in local standard time. */
+ if (zone_line->save_seconds)
+ save_seconds = zone_line->save_seconds;
+ is_daylight = save_seconds ? TRUE : FALSE;
+ vzictime_start = &g_array_index (changes, VzicTime, start_index);
+ walloff = vzictime_start->walloff = stdoff + save_seconds;
+
+ /* TEST: See if the first Rule time is exactly the same as the change from
+ the Zone line. In which case we can remove the Zone line change. */
+ if (changes->len > start_index + 1) {
+ int prev_stdoff, prev_walloff;
+
+ if (start_index > 0) {
+ VzicTime *v = &g_array_index (changes, VzicTime, start_index - 1);
+ prev_stdoff = v->stdoff;
+ prev_walloff = v->walloff;
+ } else {
+ prev_stdoff = 0;
+ prev_walloff = 0;
+ }
+ vzictime_first_rule_change = &g_array_index (changes, VzicTime,
+ start_index + 1);
+ if (times_match (vzictime_start, prev_stdoff, prev_walloff,
+ vzictime_first_rule_change, stdoff, walloff)) {
+#if 0
+ printf ("Removing zone-line change (using new offsets)\n");
+#endif
+ g_array_remove_index (changes, start_index);
+ vzictime_start = NULL;
+ } else if (times_match (vzictime_start, prev_stdoff, prev_walloff,
+ vzictime_first_rule_change, prev_stdoff, prev_walloff)) {
+#if 0
+ printf ("Removing zone-line change (using previous offsets)\n");
+#endif
+ g_array_remove_index (changes, start_index);
+ vzictime_start = NULL;
+ }
+ }
+
+
+ if (vzictime_start) {
+ vzictime_start->tzname = expand_tzname (zone_name, zone_line->format,
+ found_letter_s,
+ start_letter_s, is_daylight);
+ }
+
+ /* The start of the next Zone line is the end time of this one. */
+ start = end;
+ }
+
+ set_previous_offsets (changes);
+
+ output_zone_components (fp, zone_name, changes);
+
+ if (VzicDumpChanges)
+ dump_changes (changes_fp, zone_name, changes);
+
+ /* Free all the TZNAME fields. */
+ for (i = 0; i < changes->len; i++) {
+ vzictime = &g_array_index (changes, VzicTime, i);
+ g_free (vzictime->tzname);
+ }
+
+ g_array_free (changes, TRUE);
+}
+
+
+/* This appends any timezone changes specified by the rules associated with
+ the timezone, that happen between the start and end times.
+ It returns the letter_s field of the first STANDARD rule found in the
+ search. We need this to fill in any %s in the FORMAT field of the first
+ component of the time period (the Zone line). */
+static gboolean
+add_rule_changes (ZoneLineData *zone_line,
+ char *zone_name,
+ GArray *changes,
+ GHashTable *rule_data,
+ VzicTime *start,
+ VzicTime *end,
+ char **start_letter_s,
+ int *save_seconds)
+{
+ GArray *rule_array;
+ RuleData *rule, *prev_rule = NULL;
+ int stdoff, walloff, i, prev_stdoff, prev_walloff;
+ VzicTime vzictime;
+ gboolean is_daylight, found_start_letter_s = FALSE;
+ gboolean checked_for_previous = FALSE;
+
+ *save_seconds = 0;
+
+ rule_array = g_hash_table_lookup (rule_data, zone_line->rules);
+ if (!rule_array) {
+ fprintf (stderr, "Couldn't access rules: %s\n", zone_line->rules);
+ exit (1);
+ }
+
+ /* The stdoff is the same for all the rules. */
+ stdoff = start->stdoff;
+
+ /* The walloff changes as we go through the rules. */
+ walloff = start->walloff;
+
+ /* Get the stdoff & walloff from the last change before this period. */
+ if (changes->len >= 2) {
+ VzicTime *change = &g_array_index (changes, VzicTime, changes->len - 2);
+ prev_stdoff = change->stdoff;
+ prev_walloff = change->walloff;
+ } else {
+ prev_stdoff = prev_walloff = 0;
+ }
+
+
+ for (i = 0; i < rule_array->len; i++) {
+ rule = &g_array_index (rule_array, RuleData, i);
+
+ is_daylight = rule->save_seconds != 0 ? TRUE : FALSE;
+
+ vzictime_init (&vzictime);
+ vzictime.year = rule->from_year;
+ vzictime.month = rule->in_month;
+ vzictime.day_code = rule->on_day_code;
+ vzictime.day_number = rule->on_day_number;
+ vzictime.day_weekday = rule->on_day_weekday;
+ vzictime.time_seconds = rule->at_time_seconds;
+ vzictime.time_code = rule->at_time_code;
+ vzictime.stdoff = stdoff;
+ vzictime.walloff = stdoff + rule->save_seconds;
+ vzictime.is_infinite = (rule->to_year == YEAR_MAXIMUM) ? TRUE : FALSE;
+
+ /* If the rule time is before the given start time, skip it. */
+ if (compare_times (&vzictime, stdoff, walloff,
+ start, prev_stdoff, prev_walloff) < 0)
+ continue;
+
+ /* If the previous Rule was a daylight Rule, then we may want to use the
+ walloff from that. */
+ if (!checked_for_previous) {
+ checked_for_previous = TRUE;
+ if (i > 0) {
+ prev_rule = &g_array_index (rule_array, RuleData, i - 1);
+ if (prev_rule->save_seconds) {
+ walloff = start->walloff = stdoff + prev_rule->save_seconds;
+ *save_seconds = prev_rule->save_seconds;
+ found_start_letter_s = TRUE;
+ *start_letter_s = prev_rule->letter_s;
+#if 0
+ printf ("Could use save_seconds from previous Rule: %s\n",
+ zone_name);
+#endif
+ }
+ }
+ }
+
+ /* If an end time has been given, then if the rule time is on or after it
+ break out of the loop. */
+ if (end->year != YEAR_MAXIMUM
+ && compare_times (&vzictime, stdoff, walloff,
+ end, stdoff, walloff) >= 0)
+ break;
+
+ vzictime.tzname = expand_tzname (zone_name, zone_line->format, TRUE,
+ rule->letter_s, is_daylight);
+
+ g_array_append_val (changes, vzictime);
+
+ /* When we find the first STANDARD time we set letter_s. */
+ if (!found_start_letter_s && !is_daylight) {
+ found_start_letter_s = TRUE;
+ *start_letter_s = rule->letter_s;
+ }
+
+ /* Now that we have added the Rule, the new walloff comes into effect
+ for any following Rules. */
+ walloff = vzictime.walloff;
+ }
+
+ return found_start_letter_s;
+}
+
+
+/* This expands the Zone line FORMAT field, using the given LETTER_S from a
+ Rule line. There are 3 types of FORMAT field:
+ 1. a string with an %s in, e.g. "WE%sT". The %s is replaced with LETTER_S.
+ 2. a string with an '/' in, e.g. "CAT/CAWT". The first part is used for
+ standard time and the second part for when daylight-saving is in effect.
+ 3. a plain string, e.g. "LMT", which we leave as-is.
+ Note that (1) is the only type in which letter_s is required.
+*/
+static char*
+expand_tzname (char *zone_name,
+ char *format,
+ gboolean have_letter_s,
+ char *letter_s,
+ gboolean is_daylight)
+{
+ char *p, buffer[256], *guess = NULL;
+ int len;
+
+#if 0
+ printf ("Expanding %s with %s\n", format, letter_s);
+#endif
+
+ if (!format || !format[0]) {
+ fprintf (stderr, "Missing FORMAT\n");
+ exit (1);
+ }
+
+ /* 1. Look for a "%s". */
+ p = strchr (format, '%');
+ if (p && *(p + 1) == 's') {
+ if (!have_letter_s) {
+
+ /* NOTE: These are a few hard-coded TZNAMEs that I've looked up myself.
+ These are needed in a few places where a Zone line comes into effect
+ but no Rule has been found, so we have no LETTER_S to use.
+ I've tried to use whatever is the normal LETTER_S in the Rules for
+ the particular zone, in local standard time. */
+ if (!strcmp (zone_name, "Asia/Macao")
+ && !strcmp (format, "C%sT"))
+ guess = "CST";
+ else if (!strcmp (zone_name, "Asia/Macau")
+ && !strcmp (format, "C%sT"))
+ guess = "CST";
+ else if (!strcmp (zone_name, "Asia/Ashgabat")
+ && !strcmp (format, "ASH%sT"))
+ guess = "ASHT";
+ else if (!strcmp (zone_name, "Asia/Ashgabat")
+ && !strcmp (format, "TM%sT"))
+ guess = "TMT";
+ else if (!strcmp (zone_name, "Asia/Samarkand")
+ && !strcmp (format, "TAS%sT"))
+ guess = "TAST";
+ else if (!strcmp (zone_name, "Atlantic/Azores")
+ && !strcmp (format, "WE%sT"))
+ guess = "WET";
+ else if (!strcmp (zone_name, "Europe/Paris")
+ && !strcmp (format, "WE%sT"))
+ guess = "WET";
+ else if (!strcmp (zone_name, "Europe/Warsaw")
+ && !strcmp (format, "CE%sT"))
+ guess = "CET";
+ else if (!strcmp (zone_name, "America/Phoenix")
+ && !strcmp (format, "M%sT"))
+ guess = "MST";
+ else if (!strcmp (zone_name, "America/Nome")
+ && !strcmp (format, "Y%sT"))
+ guess = "YST";
+
+ if (guess) {
+#if 0
+ fprintf (stderr,
+ "WARNING: Couldn't find a LETTER_S to use in FORMAT: %s in Zone: %s Guessing: %s\n",
+ format, zone_name, guess);
+#endif
+ return g_strdup (guess);
+ }
+
+#if 1
+ fprintf (stderr,
+ "WARNING: Couldn't find a LETTER_S to use in FORMAT: %s in Zone: %s Leaving TZNAME empty\n",
+ format, zone_name);
+#endif
+
+#if 0
+ /* This is useful to spot exactly which component had a problem. */
+ sprintf (buffer, "FIXME: %s", format);
+ return g_strdup (buffer);
+#else
+ /* We give up and don't output a TZNAME. */
+ return NULL;
+#endif
+ }
+
+ sprintf (buffer, format, letter_s ? letter_s : "");
+ return g_strdup (buffer);
+ }
+
+ /* 2. Look for a "/". */
+ p = strchr (format, '/');
+ if (p) {
+ if (is_daylight) {
+ return g_strdup (p + 1);
+ } else {
+ len = p - format;
+ strncpy (buffer, format, len);
+ buffer[len] = '\0';
+ return g_strdup (buffer);
+ }
+ }
+
+ /* 3. Just use format as it is. */
+ return g_strdup (format);
+}
+
+
+/* Compares 2 VzicTimes, returning strcmp()-like values, i.e. 0 if equal,
+ 1 if the 1st is after the 2nd and -1 if the 1st is before the 2nd. */
+static int
+compare_times (VzicTime *time1,
+ int stdoff1,
+ int walloff1,
+ VzicTime *time2,
+ int stdoff2,
+ int walloff2)
+{
+ VzicTime t1, t2;
+ int result;
+
+ t1 = *time1;
+ t2 = *time2;
+
+ calculate_actual_time (&t1, TIME_UNIVERSAL, stdoff1, walloff1);
+ calculate_actual_time (&t2, TIME_UNIVERSAL, stdoff2, walloff2);
+
+ /* Now we can compare the entire time. */
+ if (t1.year > t2.year)
+ result = 1;
+ else if (t1.year < t2.year)
+ result = -1;
+
+ else if (t1.month > t2.month)
+ result = 1;
+ else if (t1.month < t2.month)
+ result = -1;
+
+ else if (t1.day_number > t2.day_number)
+ result = 1;
+ else if (t1.day_number < t2.day_number)
+ result = -1;
+
+ else if (t1.time_seconds > t2.time_seconds)
+ result = 1;
+ else if (t1.time_seconds < t2.time_seconds)
+ result = -1;
+
+ else
+ result = 0;
+
+#if 0
+ printf ("%i/%i/%i %i <=> %i/%i/%i %i -> %i\n",
+ t1.day_number, t1.month + 1, t1.year, t1.time_seconds,
+ t2.day_number, t2.month + 1, t2.year, t2.time_seconds,
+ result);
+#endif
+
+ return result;
+}
+
+
+/* Returns TRUE if the 2 times are exactly the same. It will calculate the
+ actual day, but doesn't convert times. */
+static gboolean
+times_match (VzicTime *time1,
+ int stdoff1,
+ int walloff1,
+ VzicTime *time2,
+ int stdoff2,
+ int walloff2)
+{
+ VzicTime t1, t2;
+
+ t1 = *time1;
+ t2 = *time2;
+
+ calculate_actual_time (&t1, TIME_UNIVERSAL, stdoff1, walloff1);
+ calculate_actual_time (&t2, TIME_UNIVERSAL, stdoff2, walloff2);
+
+ if (t1.year == t2.year
+ && t1.month == t2.month
+ && t1.day_number == t2.day_number
+ && t1.time_seconds == t2.time_seconds)
+ return TRUE;
+
+ return FALSE;
+}
+
+
+static void
+output_zone_components (FILE *fp,
+ char *name,
+ GArray *changes)
+{
+ VzicTime *vzictime;
+ int i, start_index = 0;
+ gboolean only_one_change = FALSE;
+ char start_buffer[1024];
+
+ fprintf (fp, "BEGIN:VTIMEZONE\nTZID:%s%s\n", TZIDPrefixExpanded, name);
+
+ if (VzicUrlPrefix != NULL)
+ fprintf (fp, "TZURL:%s/%s\n", VzicUrlPrefix, name);
+
+ /* We use an 'X-' property to place the city name in. */
+ fprintf (fp, "X-LIC-LOCATION:%s\n", name);
+
+ /* We try to find any recurring components first, or they may get output
+ as lots of RDATES instead. */
+ if (!VzicNoRRules) {
+ int num_rrules_output = 0;
+
+ for (i = 1; i < changes->len; i++) {
+ if (check_for_recurrence (fp, changes, i)) {
+ num_rrules_output++;
+ }
+ }
+
+#if 0
+ printf ("Zone: %s had %i infinite RRULEs\n", CurrentZoneName,
+ num_rrules_output);
+#endif
+
+ if (!VzicPureOutput && num_rrules_output == 2) {
+#if 0
+ printf ("Zone: %s using 2 RRULEs\n", CurrentZoneName);
+#endif
+ fprintf (fp, "END:VTIMEZONE\n");
+ return;
+ }
+ }
+
+ /* We skip the first change, which starts at -infinity, unless it is the only
+ change for the timezone. */
+ if (changes->len > 1)
+ start_index = 1;
+ else
+ only_one_change = TRUE;
+
+ /* For pure output, we start at the start of the array and step through it
+ outputting RDATEs. For Outlook-compatible output we start at the end
+ and step backwards to find the first STANDARD time to output. */
+ if (VzicPureOutput)
+ i = start_index - 1;
+ else
+ i = changes->len;
+
+ for (;;) {
+ if (VzicPureOutput)
+ i++;
+ else
+ i--;
+
+ if (VzicPureOutput) {
+ if (i >= changes->len)
+ break;
+ } else {
+ if (i < start_index)
+ break;
+ }
+
+ vzictime = &g_array_index (changes, VzicTime, i);
+
+ /* If we have already output this component as part of an RRULE or RDATE,
+ then we skip it. */
+ if (vzictime->output)
+ continue;
+
+ /* For Outlook-compatible output we only want to output the last STANDARD
+ time as a DTSTART, so skip any DAYLIGHT changes. */
+ if (!VzicPureOutput && vzictime->stdoff != vzictime->walloff) {
+ printf ("Skipping DAYLIGHT change\n");
+ continue;
+ }
+
+#if 0
+ printf ("Zone: %s using DTSTART Year: %i\n", CurrentZoneName,
+ vzictime->year);
+#endif
+
+ if (VzicPureOutput) {
+ output_component_start (start_buffer, vzictime, TRUE, only_one_change);
+ } else {
+ /* For Outlook compatability we don't output the RDATE and use the same
+ TZOFFSET for TZOFFSETFROM and TZOFFSETTO. */
+ vzictime->year = RDATE_YEAR;
+ vzictime->month = 0;
+ vzictime->day_code = DAY_SIMPLE;
+ vzictime->day_number = 1;
+ vzictime->time_code = TIME_WALL;
+ vzictime->time_seconds = 0;
+
+ output_component_start (start_buffer, vzictime, FALSE, TRUE);
+ }
+
+ fprintf (fp, "%s", start_buffer);
+
+ /* This will look for matching components and output them as RDATEs
+ instead of separate components. */
+ if (VzicPureOutput && !VzicNoRDates)
+ check_for_rdates (fp, changes, i);
+
+ output_component_end (fp, vzictime);
+
+ vzictime->output = TRUE;
+
+ if (!VzicPureOutput)
+ break;
+ }
+
+ fprintf (fp, "END:VTIMEZONE\n");
+}
+
+
+/* This sets the prev_stdoff and prev_walloff (i.e. the TZOFFSETFROM) of each
+ VzicTime, using the stdoff and walloff of the previous VzicTime. It makes
+ the rest of the code much simpler. */
+static void
+set_previous_offsets (GArray *changes)
+{
+ VzicTime *vzictime, *prev_vzictime;
+ int i;
+
+ prev_vzictime = &g_array_index (changes, VzicTime, 0);
+ prev_vzictime->prev_stdoff = 0;
+ prev_vzictime->prev_walloff = 0;
+
+ for (i = 1; i < changes->len; i++) {
+ vzictime = &g_array_index (changes, VzicTime, i);
+
+ vzictime->prev_stdoff = prev_vzictime->stdoff;
+ vzictime->prev_walloff = prev_vzictime->walloff;
+
+ prev_vzictime = vzictime;
+ }
+}
+
+
+/* Returns TRUE if we output an infinite recurrence. */
+static gboolean
+check_for_recurrence (FILE *fp,
+ GArray *changes,
+ int idx)
+{
+ VzicTime *vzictime_start, *vzictime, vzictime_start_copy;
+ gboolean is_daylight_start, is_daylight;
+ int last_match, i, next_year, day_offset;
+ char until[256], rrule_buffer[2048], start_buffer[1024];
+ GList *matching_elements = NULL, *elem;
+
+ vzictime_start = &g_array_index (changes, VzicTime, idx);
+
+ /* If this change has already been output, skip it. */
+ if (vzictime_start->output)
+ return FALSE;
+
+ /* There can't possibly be an RRULE starting from YEAR_MINIMUM. */
+ if (vzictime_start->year == YEAR_MINIMUM)
+ return FALSE;
+
+ is_daylight_start = (vzictime_start->stdoff != vzictime_start->walloff)
+ ? TRUE : FALSE;
+
+#if 0
+ printf ("\nChecking: %s OFFSETFROM: %i %s\n",
+ format_vzictime (vzictime_start), vzictime_start->prev_walloff,
+ is_daylight_start ? "DAYLIGHT" : "");
+#endif
+
+ /* If this is an infinitely recurring change, output the RRULE and return.
+ There won't be any changes after it that we could merge. */
+ if (vzictime_start->is_infinite) {
+
+ /* Change the year to our minimum start year. */
+ vzictime_start_copy = *vzictime_start;
+ if (!VzicPureOutput)
+ vzictime_start_copy.year = RRULE_START_YEAR;
+
+ day_offset = output_component_start (start_buffer, &vzictime_start_copy,
+ FALSE, FALSE);
+
+ if (!output_rrule (rrule_buffer, vzictime_start_copy.month,
+ vzictime_start_copy.day_code,
+ vzictime_start_copy.day_number,
+ vzictime_start_copy.day_weekday, day_offset, "")) {
+ if (vzictime_start->year != MAX_TIME_T_YEAR) {
+ fprintf (stderr, "WARNING: Failed to output infinite recurrence with start year: %i\n", vzictime_start->year);
+ }
+ return TRUE;
+ }
+
+ fprintf (fp, "%s%s", start_buffer, rrule_buffer);
+ output_component_end (fp, vzictime_start);
+ vzictime_start->output = TRUE;
+ return TRUE;
+ }
+
+ last_match = idx;
+ next_year = vzictime_start->year + 1;
+ for (i = idx + 1; i < changes->len; i++) {
+ vzictime = &g_array_index (changes, VzicTime, i);
+
+ is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE;
+
+ if (vzictime->output)
+ continue;
+
+#if 0
+ printf (" %s OFFSETFROM: %i %s\n",
+ format_vzictime (vzictime), vzictime->prev_walloff,
+ is_daylight ? "DAYLIGHT" : "");
+#endif
+
+ /* If it is more than one year ahead, we are finished, since we want
+ consecutive years. */
+ if (vzictime->year > next_year) {
+ break;
+ }
+
+ /* It must be the same type of component - STANDARD or DAYLIGHT. */
+ if (is_daylight != is_daylight_start) {
+ continue;
+ }
+
+ /* It must be the following year, with the same month, day & time.
+ It is possible that the time has a different code but does in fact
+ match when normalized, but we don't care (for now at least). */
+ if (vzictime->year != next_year
+ || vzictime->month != vzictime_start->month
+ || vzictime->day_code != vzictime_start->day_code
+ || vzictime->day_number != vzictime_start->day_number
+ || vzictime->day_weekday != vzictime_start->day_weekday
+ || vzictime->time_seconds != vzictime_start->time_seconds
+ || vzictime->time_code != vzictime_start->time_code) {
+ continue;
+ }
+
+ /* The TZOFFSETFROM and TZOFFSETTO must match. */
+ if (vzictime->prev_walloff != vzictime_start->prev_walloff) {
+ continue;
+ }
+
+ if (vzictime->walloff != vzictime_start->walloff) {
+ continue;
+ }
+
+ /* TZNAME must match. */
+ if (!timezones_match (vzictime->tzname, vzictime_start->tzname)) {
+ continue;
+ }
+
+ /* We have a match. */
+ last_match = i;
+ next_year = vzictime->year + 1;
+
+ matching_elements = g_list_prepend (matching_elements, vzictime);
+ }
+
+ if (last_match == idx)
+ return FALSE;
+
+#if 0
+ printf ("Found recurrence %i - %i!!!\n", vzictime_start->year,
+ next_year - 1);
+#endif
+
+ vzictime = &g_array_index (changes, VzicTime, last_match);
+
+/* We only use RRULEs if there are at least MIN_RRULE_OCCURRENCES occurrences,
+ since otherwise RDATEs are more efficient. */
+ if (!vzictime->is_infinite) {
+ int years = vzictime->year - vzictime_start->year + 1;
+#if 0
+ printf ("RRULE Years: %i\n", years);
+#endif
+ if (years < MIN_RRULE_OCCURRENCES)
+ return FALSE;
+ }
+
+ if (vzictime->is_infinite) {
+ until[0] = '\0';
+ } else {
+ VzicTime t1 = *vzictime;
+
+ printf ("RRULE with UNTIL - aborting\n");
+ abort ();
+
+ calculate_actual_time (&t1, TIME_UNIVERSAL, vzictime->prev_stdoff,
+ vzictime->prev_walloff);
+
+ /* Output UNTIL, in UTC. */
+ sprintf (until, ";UNTIL=%sZ", format_time (t1.year, t1.month,
+ t1.day_number,
+ t1.time_seconds));
+ }
+
+ /* Change the year to our minimum start year. */
+ vzictime_start_copy = *vzictime_start;
+ if (!VzicPureOutput)
+ vzictime_start_copy.year = RRULE_START_YEAR;
+
+ day_offset = output_component_start (start_buffer, &vzictime_start_copy,
+ FALSE, FALSE);
+ if (output_rrule (rrule_buffer, vzictime_start_copy.month,
+ vzictime_start_copy.day_code,
+ vzictime_start_copy.day_number,
+ vzictime_start_copy.day_weekday, day_offset, until)) {
+ fprintf (fp, "%s%s", start_buffer, rrule_buffer);
+ output_component_end (fp, vzictime_start);
+
+ /* Mark all the changes as output. */
+ vzictime_start->output = TRUE;
+ for (elem = matching_elements; elem; elem = elem->next) {
+ vzictime = elem->data;
+ vzictime->output = TRUE;
+ }
+ }
+
+ g_list_free (matching_elements);
+
+ return TRUE;
+}
+
+
+static void
+check_for_rdates (FILE *fp,
+ GArray *changes,
+ int idx)
+{
+ VzicTime *vzictime_start, *vzictime, tmp_vzictime;
+ gboolean is_daylight_start, is_daylight;
+ int i, year, month, day, time;
+
+ vzictime_start = &g_array_index (changes, VzicTime, idx);
+
+ is_daylight_start = (vzictime_start->stdoff != vzictime_start->walloff)
+ ? TRUE : FALSE;
+
+#if 0
+ printf ("\nChecking: %s OFFSETFROM: %i %s\n",
+ format_vzictime (vzictime_start), vzictime_start->prev_walloff,
+ is_daylight_start ? "DAYLIGHT" : "");
+#endif
+
+ /* We want to go backwards through the array now, for Outlook compatability.
+ (It only looks at the first DTSTART/RDATE.) */
+ for (i = idx + 1; i < changes->len; i++) {
+ vzictime = &g_array_index (changes, VzicTime, i);
+
+ is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE;
+
+ if (vzictime->output)
+ continue;
+
+#if 0
+ printf (" %s OFFSETFROM: %i %s\n", format_vzictime (vzictime),
+ vzictime->prev_walloff, is_daylight ? "DAYLIGHT" : "");
+#endif
+
+ /* It must be the same type of component - STANDARD or DAYLIGHT. */
+ if (is_daylight != is_daylight_start) {
+ continue;
+ }
+
+ /* The TZOFFSETFROM and TZOFFSETTO must match. */
+ if (vzictime->prev_walloff != vzictime_start->prev_walloff) {
+ continue;
+ }
+
+ if (vzictime->walloff != vzictime_start->walloff) {
+ continue;
+ }
+
+ /* TZNAME must match. */
+ if (!timezones_match (vzictime->tzname, vzictime_start->tzname)) {
+ continue;
+ }
+
+ /* We have a match. */
+
+ tmp_vzictime = *vzictime;
+ calculate_actual_time (&tmp_vzictime, TIME_WALL, vzictime->prev_stdoff,
+ vzictime->prev_walloff);
+
+ fprintf (fp, "RDATE:%s\n", format_time (tmp_vzictime.year,
+ tmp_vzictime.month,
+ tmp_vzictime.day_number,
+ tmp_vzictime.time_seconds));
+
+ vzictime->output = TRUE;
+ }
+}
+
+
+static gboolean
+timezones_match (char *tzname1,
+ char *tzname2)
+{
+ if (tzname1 && tzname2 && !strcmp (tzname1, tzname2))
+ return TRUE;
+
+ if (!tzname1 && !tzname2)
+ return TRUE;
+
+ return FALSE;
+}
+
+
+/* Outputs the start of a VTIMEZONE component, with the BEGIN line,
+ the DTSTART, TZOFFSETFROM, TZOFFSETTO & TZNAME properties. */
+static int
+output_component_start (char *buffer,
+ VzicTime *vzictime,
+ gboolean output_rdate,
+ gboolean use_same_tz_offset)
+{
+ gboolean is_daylight, skip_day_offset = FALSE;
+ gint year, month, day, time, day_offset = 0;
+ GDate old_date, new_date;
+ char *formatted_time;
+ char line1[1024], line2[1024], line3[1024];
+ char line4[1024], line5[1024], line6[1024];
+ VzicTime tmp_vzictime;
+ int prev_walloff;
+
+ is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE;
+
+ tmp_vzictime = *vzictime;
+ day_offset = calculate_actual_time (&tmp_vzictime, TIME_WALL,
+ vzictime->prev_stdoff,
+ vzictime->prev_walloff);
+
+ sprintf (line1, "BEGIN:%s\n", is_daylight ? "DAYLIGHT" : "STANDARD");
+
+ /* If the timezone only has one change, that means it uses the same offset
+ forever, so we use the same TZOFFSETFROM as the TZOFFSETTO. (If the zone
+ has more than one change, we don't output the first one.) */
+ if (use_same_tz_offset)
+ prev_walloff = vzictime->walloff;
+ else
+ prev_walloff = vzictime->prev_walloff;
+
+ sprintf (line2, "TZOFFSETFROM:%s\n",
+ format_tz_offset (prev_walloff, !VzicPureOutput));
+
+ sprintf (line3, "TZOFFSETTO:%s\n",
+ format_tz_offset (vzictime->walloff, !VzicPureOutput));
+
+ if (vzictime->tzname)
+ sprintf (line4, "TZNAME:%s\n", vzictime->tzname);
+ else
+ line4[0] = '\0';
+
+ formatted_time = format_time (tmp_vzictime.year, tmp_vzictime.month,
+ tmp_vzictime.day_number,
+ tmp_vzictime.time_seconds);
+ sprintf (line5, "DTSTART:%s\n", formatted_time);
+ if (output_rdate)
+ sprintf (line6, "RDATE:%s\n", formatted_time);
+ else
+ line6[0] = '\0';
+
+ sprintf (buffer, "%s%s%s%s%s%s", line1, line2, line3, line4, line5, line6);
+
+ return day_offset;
+}
+
+
+/* Outputs the END line of the VTIMEZONE component. */
+static void
+output_component_end (FILE *fp,
+ VzicTime *vzictime)
+{
+ gboolean is_daylight;
+
+ is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE;
+
+ fprintf (fp, "END:%s\n", is_daylight ? "DAYLIGHT" : "STANDARD");
+}
+
+
+/* Initializes a VzicTime to 1st Jan in YEAR_MINIMUM at midnight, with all
+ offsets set to 0. */
+static void
+vzictime_init (VzicTime *vzictime)
+{
+ vzictime->year = YEAR_MINIMUM;
+ vzictime->month = 0;
+ vzictime->day_code = DAY_SIMPLE;
+ vzictime->day_number = 1;
+ vzictime->day_weekday = 0;
+ vzictime->time_seconds = 0;
+ vzictime->time_code = TIME_UNIVERSAL;
+ vzictime->stdoff = 0;
+ vzictime->walloff = 0;
+ vzictime->is_infinite = FALSE;
+ vzictime->output = FALSE;
+ vzictime->prev_stdoff = 0;
+ vzictime->prev_walloff = 0;
+ vzictime->tzname = NULL;
+}
+
+
+/* This calculates the actual local time that a change will occur, given
+ the offsets from standard and wall-clock time. It returns -1 or 1 if it
+ had to move backwards or forwards one day while converting to local time.
+ If it does this then we need to change the RRULEs we output. */
+static int
+calculate_actual_time (VzicTime *vzictime,
+ TimeCode time_code,
+ int stdoff,
+ int walloff)
+{
+ GDate date;
+ gint day_offset, days_in_month, weekday, offset, result;
+
+ vzictime->time_seconds = calculate_wall_time (vzictime->time_seconds,
+ vzictime->time_code,
+ stdoff, walloff, &day_offset);
+
+ if (vzictime->day_code != DAY_SIMPLE) {
+ if (vzictime->year == YEAR_MINIMUM || vzictime->year == YEAR_MAXIMUM) {
+ fprintf (stderr, "In calculate_actual_time: invalid year\n");
+ exit (0);
+ }
+
+ g_date_clear (&date, 1);
+ days_in_month = g_date_days_in_month (vzictime->month + 1, vzictime->year);
+
+ /* Note that the day_code refers to the date before we convert it to
+ a wall-clock date and time. So we find the day it was referring to,
+ then make any adjustments needed due to converting the time. */
+ if (vzictime->day_code == DAY_LAST_WEEKDAY) {
+ /* Find out what day the last day of the month is. */
+ g_date_set_dmy (&date, days_in_month, vzictime->month + 1,
+ vzictime->year);
+ weekday = g_date_weekday (&date) % 7;
+
+ /* Calculate how many days we have to go back to get to day_weekday. */
+ offset = (weekday + 7 - vzictime->day_weekday) % 7;
+
+ vzictime->day_number = days_in_month - offset;
+ } else {
+ /* Find out what day day_number actually is. */
+ g_date_set_dmy (&date, vzictime->day_number, vzictime->month + 1,
+ vzictime->year);
+ weekday = g_date_weekday (&date) % 7;
+
+ if (vzictime->day_code == DAY_WEEKDAY_ON_OR_AFTER)
+ offset = (vzictime->day_weekday + 7 - weekday) % 7;
+ else
+ offset = - ((weekday + 7 - vzictime->day_weekday) % 7);
+
+ vzictime->day_number = vzictime->day_number + offset;
+ }
+
+ vzictime->day_code = DAY_SIMPLE;
+
+ if (vzictime->day_number <= 0 || vzictime->day_number > days_in_month) {
+ fprintf (stderr, "Day overflow: %i\n", vzictime->day_number);
+ exit (1);
+ }
+ }
+
+#if 0
+ fprintf (stderr, "%s -> %i/%i/%i\n",
+ dump_day_coded (vzictime->day_code, vzictime->day_number,
+ vzictime->day_weekday),
+ vzictime->day_number, vzictime->month + 1, vzictime->year);
+#endif
+
+ fix_time_overflow (&vzictime->year, &vzictime->month,
+ &vzictime->day_number, day_offset);
+
+ /* If we want UTC time, we have to convert it now. */
+ if (time_code == TIME_UNIVERSAL) {
+ vzictime->time_seconds = calculate_until_time (vzictime->time_seconds,
+ TIME_WALL, stdoff, walloff,
+ &vzictime->year,
+ &vzictime->month,
+ &vzictime->day_number);
+ }
+
+ return day_offset;
+}
+
+
+/* This converts the given time into universal time (UTC), to be used in
+ the UNTIL property. */
+static int
+calculate_until_time (int time,
+ TimeCode time_code,
+ int stdoff,
+ int walloff,
+ int *year,
+ int *month,
+ int *day)
+{
+ int result, day_offset;
+
+ day_offset = 0;
+
+ switch (time_code) {
+ case TIME_WALL:
+ result = time - walloff;
+ break;
+ case TIME_STANDARD:
+ result = time - stdoff;
+ break;
+ case TIME_UNIVERSAL:
+ return time;
+ default:
+ fprintf (stderr, "Invalid time code\n");
+ exit (1);
+ }
+
+ if (result < 0) {
+ result += 24 * 60 * 60;
+ day_offset = -1;
+ } else if (result >= 24 * 60 * 60) {
+ result -= 24 * 60 * 60;
+ day_offset = 1;
+ }
+
+ /* Sanity check - we shouldn't have an overflow any more. */
+ if (result < 0 || result >= 24 * 60 * 60) {
+ fprintf (stderr, "Time overflow: %i\n", result);
+ abort ();
+ }
+
+ fix_time_overflow (year, month, day, day_offset);
+
+ return result;
+}
+
+
+/* This converts the given time into wall clock time (the local standard time
+ with any adjustment for daylight-saving). */
+static int
+calculate_wall_time (int time,
+ TimeCode time_code,
+ int stdoff,
+ int walloff,
+ int *day_offset)
+{
+ int result;
+
+ *day_offset = 0;
+
+ switch (time_code) {
+ case TIME_WALL:
+ return time;
+ case TIME_STANDARD:
+ /* We have a local standard time, so we have to subtract stdoff to get
+ back to UTC, then add walloff to get wall time. */
+ result = time - stdoff + walloff;
+ break;
+ case TIME_UNIVERSAL:
+ result = time + walloff;
+ break;
+ default:
+ fprintf (stderr, "Invalid time code\n");
+ exit (1);
+ }
+
+ if (result < 0) {
+ result += 24 * 60 * 60;
+ *day_offset = -1;
+ } else if (result >= 24 * 60 * 60) {
+ result -= 24 * 60 * 60;
+ *day_offset = 1;
+ }
+
+ /* Sanity check - we shouldn't have an overflow any more. */
+ if (result < 0 || result >= 24 * 60 * 60) {
+ fprintf (stderr, "Time overflow: %i\n", result);
+ exit (1);
+ }
+
+#if 0
+ printf ("%s -> ", dump_time (time, time_code, TRUE));
+ printf ("%s (%i)\n", dump_time (result, TIME_WALL, TRUE), *day_offset);
+#endif
+
+ return result;
+}
+
+
+static void
+fix_time_overflow (int *year,
+ int *month,
+ int *day,
+ int day_offset)
+{
+ if (day_offset == -1) {
+ *day = *day - 1;
+
+ if (*day == 0) {
+ *month = *month - 1;
+ if (*month == -1) {
+ *month = 11;
+ *year = *year - 1;
+ }
+ *day = g_date_days_in_month (*month + 1, *year);
+ }
+ } else if (day_offset == 1) {
+ *day = *day + 1;
+
+ if (*day > g_date_days_in_month (*month + 1, *year)) {
+ *month = *month + 1;
+ if (*month == 12) {
+ *month = 0;
+ *year = *year + 1;
+ }
+ *day = 1;
+ }
+ }
+}
+
+
+static char*
+format_time (int year,
+ int month,
+ int day,
+ int time)
+{
+ static char buffer[128];
+ int hour, minute, second;
+
+ /* When we are outputting the first component year will be YEAR_MINIMUM.
+ We used to use 1 when outputting this, but Outlook doesn't like any years
+ less that 1600, so we use 1600 instead. We don't output the first change
+ for most zones now, so it doesn't matter too much. */
+ if (year == YEAR_MINIMUM)
+ year = 1601;
+
+ /* We just use 9999 here, so we keep to 4 characters. But this should only
+ be needed when debugging - it shouldn't be needed in the VTIMEZONEs. */
+ if (year == YEAR_MAXIMUM) {
+ fprintf (stderr, "format_time: YEAR_MAXIMUM used\n");
+ year = 9999;
+ }
+
+ hour = time / 3600;
+ minute = (time % 3600) / 60;
+ second = time % 60;
+
+ sprintf (buffer, "%04i%02i%02iT%02i%02i%02i",
+ year, month + 1, day, hour, minute, second);
+
+ return buffer;
+}
+
+
+/* Outlook doesn't support 6-digit values, i.e. including the seconds, so
+ we round to the nearest minute. No current offsets use the seconds value,
+ so we aren't losing much. */
+static char*
+format_tz_offset (int tz_offset,
+ gboolean round_seconds)
+{
+ static char buffer[128];
+ char *sign = "+";
+ int hours, minutes, seconds;
+
+ if (tz_offset < 0) {
+ tz_offset = -tz_offset;
+ sign = "-";
+ }
+
+ if (round_seconds)
+ tz_offset += 30;
+
+ hours = tz_offset / 3600;
+ minutes = (tz_offset % 3600) / 60;
+ seconds = tz_offset % 60;
+
+ if (round_seconds)
+ seconds = 0;
+
+ /* Sanity check. Standard timezone offsets shouldn't be much more than 12
+ hours, and daylight saving shouldn't change it by more than a few hours.
+ (The maximum offset is 15 hours 56 minutes at present.) */
+ if (hours < 0 || hours >= 24 || minutes < 0 || minutes >= 60
+ || seconds < 0 || seconds >= 60) {
+ fprintf (stderr, "WARNING: Strange timezone offset: H:%i M:%i S:%i\n",
+ hours, minutes, seconds);
+ }
+
+ if (seconds == 0)
+ sprintf (buffer, "%s%02i%02i", sign, hours, minutes);
+ else
+ sprintf (buffer, "%s%02i%02i%02i", sign, hours, minutes, seconds);
+
+ return buffer;
+}
+
+
+static gboolean
+output_rrule (char *rrule_buffer,
+ int month,
+ DayCode day_code,
+ int day_number,
+ int day_weekday,
+ int day_offset,
+ char *until)
+{
+ char buffer[1024], buffer2[1024];
+
+ buffer[0] = '\0';
+
+ if (day_offset > 1 || day_offset < -1) {
+ fprintf (stderr, "Invalid day_offset: %i\n", day_offset);
+ exit (0);
+ }
+
+ /* If the DTSTART time was moved to another day when converting to local
+ time, we need to adjust the RRULE accordingly. e.g. If the original RRULE
+ was on the 19th of the month, but DTSTART was moved 1 day forward, then
+ we output the 20th of the month instead. */
+ if (day_offset == 1) {
+ if (day_code != DAY_LAST_WEEKDAY)
+ day_number++;
+ day_weekday = (day_weekday + 1) % 7;
+
+ /* Check we don't use February 29th. */
+ if (month == 1 && day_number > 28) {
+ fprintf (stderr, "Can't format RRULE - out of bounds. Month: %i Day number: %i\n", month + 1, day_number);
+ exit (0);
+ }
+
+ /* If we go past the end of the month, move to the next month. */
+ if (day_code != DAY_LAST_WEEKDAY && day_number > DaysInMonth[month]) {
+ month++;
+ day_number = 1;
+ }
+
+ } else if (day_offset == -1) {
+ if (day_code != DAY_LAST_WEEKDAY)
+ day_number--;
+ day_weekday = (day_weekday + 6) % 7;
+
+ if (day_code != DAY_LAST_WEEKDAY && day_number < 1)
+ fprintf (stderr, "Month: %i Day number: %i\n", month + 1, day_number);
+ }
+
+ switch (day_code) {
+ case DAY_SIMPLE:
+ /* Outlook (2000) will not parse the simple YEARLY RRULEs in VTIMEZONEs,
+ or BYMONTHDAY, or BYYEARDAY, which makes this option difficult!
+ Currently we use something like BYDAY=1SU, which will be incorrect
+ at times. This only affects Asia/Baghdad, Asia/Gaza, Asia/Jerusalem &
+ Asia/Damascus at present (and Jerusalem doesn't have specific rules
+ at the moment anyway, so that isn't a big loss). */
+ if (!VzicPureOutput) {
+ if (day_number < 8) {
+ printf ("WARNING: %s: Outputting BYDAY=1SU instead of BYMONTHDAY=1-7 for Outlook compatability\n", CurrentZoneName);
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1SU",
+ month + 1);
+ } else if (day_number < 15) {
+ printf ("WARNING: %s: Outputting BYDAY=2SU instead of BYMONTHDAY=8-14 for Outlook compatability\n", CurrentZoneName);
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2SU",
+ month + 1);
+ } else if (day_number < 22) {
+ printf ("WARNING: %s: Outputting BYDAY=3SU instead of BYMONTHDAY=15-21 for Outlook compatability\n", CurrentZoneName);
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=3SU",
+ month + 1);
+ } else {
+ printf ("ERROR: %s: Couldn't output RRULE (day=%i) compatible with Outlook\n", CurrentZoneName, day_number);
+ exit (1);
+ }
+ } else {
+ sprintf (buffer, "RRULE:FREQ=YEARLY");
+ }
+ break;
+
+ case DAY_WEEKDAY_ON_OR_AFTER:
+ if (day_number > DaysInMonth[month] - 6) {
+ /* This isn't actually needed at present. */
+#if 0
+ fprintf (stderr, "DAY_WEEKDAY_ON_OR_AFTER: %i %i\n", day_number,
+ month + 1);
+#endif
+
+ if (!VzicPureOutput) {
+ printf ("ERROR: %s: Couldn't output RRULE (day>=x) compatible with Outlook\n", CurrentZoneName);
+ exit (1);
+ } else {
+ /* We do 6 days at the end of this month, and 1 at the start of the
+ next. We can't do this if we want Outlook compatability, as it
+ needs BYMONTHDAY, which Outlook doesn't support. */
+ sprintf (buffer,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i;BYDAY=%s",
+ month + 1,
+ day_number, day_number + 1, day_number + 2, day_number + 3,
+ day_number + 4, day_number + 5,
+ WeekDays[day_weekday]);
+
+ sprintf (buffer2,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=1;BYDAY=%s",
+ (month + 1) % 12 + 1,
+ WeekDays[day_weekday]);
+
+ sprintf (rrule_buffer, "%s%s\n%s%s\n",
+ buffer, until, buffer2, until);
+
+ return TRUE;
+ }
+ }
+
+ if (!output_rrule_2 (buffer, month, day_number, day_weekday))
+ return FALSE;
+
+ break;
+
+ case DAY_WEEKDAY_ON_OR_BEFORE:
+ if (day_number < 7) {
+ /* FIXME: This is unimplemented, but it isn't needed at present anway. */
+ fprintf (stderr, "DAY_WEEKDAY_ON_OR_BEFORE: %i. Unimplemented. Exiting...\n", day_number);
+ exit (0);
+ }
+
+ if (!output_rrule_2 (buffer, month, day_number - 6, day_weekday))
+ return FALSE;
+
+ break;
+
+ case DAY_LAST_WEEKDAY:
+ if (day_offset == 1) {
+ if (month == 1) {
+ fprintf (stderr, "DAY_LAST_WEEKDAY - day moved, in February - can't fix\n");
+ exit (0);
+ }
+
+ /* This is only used once at present, for Africa/Cairo. */
+#if 0
+ fprintf (stderr, "DAY_LAST_WEEKDAY - day moved\n");
+#endif
+
+ if (!VzicPureOutput) {
+ printf ("WARNING: %s: Modifying RRULE (last weekday) for Outlook compatability\n", CurrentZoneName);
+ sprintf (buffer,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s",
+ month + 1, WeekDays[day_weekday]);
+ printf (" Outputting: %s\n", buffer);
+ } else {
+ /* We do 6 days at the end of this month, and 1 at the start of the
+ next. We can't do this if we want Outlook compatability, as it needs
+ BYMONTHDAY, which Outlook doesn't support. */
+ day_number = DaysInMonth[month];
+ sprintf (buffer,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i;BYDAY=%s",
+ month + 1,
+ day_number - 5, day_number - 4, day_number - 3,
+ day_number - 2, day_number - 1, day_number,
+ WeekDays[day_weekday]);
+
+ sprintf (buffer2,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=1;BYDAY=%s",
+ (month + 1) % 12 + 1,
+ WeekDays[day_weekday]);
+
+ sprintf (rrule_buffer, "%s%s\n%s%s\n",
+ buffer, until, buffer2, until);
+
+ return TRUE;
+ }
+
+ } else if (day_offset == -1) {
+ /* We do 7 days 1 day before the end of this month. */
+ day_number = DaysInMonth[month];
+
+ if (!output_rrule_2 (buffer, month, day_number - 7, day_weekday))
+ return FALSE;
+
+ sprintf (rrule_buffer, "%s%s\n", buffer, until);
+ return TRUE;
+ }
+
+ sprintf (buffer,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s",
+ month + 1, WeekDays[day_weekday]);
+ break;
+
+ default:
+ fprintf (stderr, "Invalid day code\n");
+ exit (1);
+ }
+
+ sprintf (rrule_buffer, "%s%s\n", buffer, until);
+ return TRUE;
+}
+
+
+/* This tries to convert a RRULE like 'BYMONTHDAY=8,9,10,11,12,13,14;BYDAY=FR'
+ into 'BYDAY=2FR'. We need this since Outlook doesn't accept BYMONTHDAY.
+ It returns FALSE if conversion is not possible. */
+static gboolean
+output_rrule_2 (char *buffer,
+ int month,
+ int day_number,
+ int day_weekday)
+{
+
+ if (day_number == 1) {
+ /* Convert it to a BYDAY=1SU type of RRULE. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1%s",
+ month + 1, WeekDays[day_weekday]);
+
+ } else if (day_number == 8) {
+ /* Convert it to a BYDAY=2SU type of RRULE. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2%s",
+ month + 1, WeekDays[day_weekday]);
+
+ } else if (day_number == 15) {
+ /* Convert it to a BYDAY=3SU type of RRULE. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=3%s",
+ month + 1, WeekDays[day_weekday]);
+
+ } else if (day_number == 22) {
+ /* Convert it to a BYDAY=4SU type of RRULE. (Currently not used.) */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=4%s",
+ month + 1, WeekDays[day_weekday]);
+
+ } else if (month != 1 && day_number == DaysInMonth[month] - 6) {
+ /* Convert it to a BYDAY=-1SU type of RRULE. (But never for February.) */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s",
+ month + 1, WeekDays[day_weekday]);
+
+ } else {
+ /* Can't convert to a correct RRULE. If we want Outlook compatability we
+ have to use a slightly incorrect RRULE, so the time change will be 1
+ week out every 7 or so years. Alternatively we could possibly move the
+ change by an hour or so so we would always be 1 or 2 hours out, but
+ never 1 week out. Yes, that sounds a better idea. */
+ if (!VzicPureOutput) {
+ printf ("WARNING: %s: Modifying RRULE to be compatible with Outlook (day >= %i, month = %i)\n", CurrentZoneName, day_number, month + 1);
+
+ if (day_number == 2) {
+ /* Convert it to a BYDAY=1SU type of RRULE.
+ This is needed for Asia/Karachi. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1%s",
+ month + 1, WeekDays[day_weekday]);
+ } else if (day_number == 9) {
+ /* Convert it to a BYDAY=2SU type of RRULE.
+ This is needed for Antarctica/Palmer & America/Santiago. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2%s",
+ month + 1, WeekDays[day_weekday]);
+ } else if (month != 1 && day_number == DaysInMonth[month] - 7) {
+ /* Convert it to a BYDAY=-1SU type of RRULE. (But never for February.)
+ This is needed for America/Godthab. */
+ sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s",
+ month + 1, WeekDays[day_weekday]);
+ } else {
+ printf ("ERROR: %s: Couldn't modify RRULE to be compatible with Outlook (day >= %i, month = %i)\n", CurrentZoneName, day_number, month + 1);
+ exit (1);
+ }
+
+ } else {
+ sprintf (buffer,
+ "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i,%i;BYDAY=%s",
+ month + 1,
+ day_number, day_number + 1, day_number + 2, day_number + 3,
+ day_number + 4, day_number + 5, day_number + 6,
+ WeekDays[day_weekday]);
+ }
+ }
+
+ return TRUE;
+}
+
+
+static char*
+format_vzictime (VzicTime *vzictime)
+{
+ static char buffer[1024];
+
+ sprintf (buffer, "%s %2i %s %s %i %i %s",
+ dump_year (vzictime->year), vzictime->month + 1,
+ dump_day_coded (vzictime->day_code, vzictime->day_number,
+ vzictime->day_weekday),
+ dump_time (vzictime->time_seconds, vzictime->time_code, TRUE),
+ vzictime->stdoff, vzictime->walloff,
+ vzictime->is_infinite ? "INFINITE" : "");
+
+ return buffer;
+}
+
+
+static void
+dump_changes (FILE *fp,
+ char *zone_name,
+ GArray *changes)
+{
+ VzicTime *vzictime, *vzictime2 = NULL;
+ int i, year_offset, year;
+
+ for (i = 0; i < changes->len; i++) {
+ vzictime = &g_array_index (changes, VzicTime, i);
+
+ if (vzictime->year > MAX_CHANGES_YEAR)
+ return;
+
+ dump_change (fp, zone_name, vzictime, vzictime->year);
+ }
+
+ if (changes->len < 2)
+ return;
+
+ /* Now see if the changes array ends with a pair of recurring changes. */
+ vzictime = &g_array_index (changes, VzicTime, changes->len - 2);
+ vzictime2 = &g_array_index (changes, VzicTime, changes->len - 1);
+ if (!vzictime->is_infinite || !vzictime2->is_infinite)
+ return;
+
+ year_offset = 1;
+ for (;;) {
+ year = vzictime->year + year_offset;
+ if (year > MAX_CHANGES_YEAR)
+ break;
+ dump_change (fp, zone_name, vzictime, year);
+
+ year = vzictime2->year + year_offset;
+ if (year > MAX_CHANGES_YEAR)
+ break;
+ dump_change (fp, zone_name, vzictime2, year);
+
+ year_offset++;
+ }
+}
+
+
+static void
+dump_change (FILE *fp,
+ char *zone_name,
+ VzicTime *vzictime,
+ int year)
+{
+ int hour, minute, second;
+ VzicTime tmp_vzictime;
+ static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ /* Output format is:
+
+ Zone-Name [tab] Date [tab] Time [tab] UTC-Offset
+
+ The Date and Time fields specify the time change in UTC.
+
+ The UTC Offset is for local (wall-clock) time. It is the amount of time
+ to add to UTC to get local time.
+ */
+
+ fprintf (fp, "%s\t", zone_name);
+
+ if (year == YEAR_MINIMUM) {
+ fprintf (fp, " 1 Jan 0001\t 0:00:00", zone_name);
+ } else if (year == YEAR_MAXIMUM) {
+ fprintf (stderr, "Maximum year found in change time\n");
+ exit (1);
+ } else {
+ tmp_vzictime = *vzictime;
+ tmp_vzictime.year = year;
+ calculate_actual_time (&tmp_vzictime, TIME_UNIVERSAL,
+ vzictime->prev_stdoff, vzictime->prev_walloff);
+
+ hour = tmp_vzictime.time_seconds / 3600;
+ minute = (tmp_vzictime.time_seconds % 3600) / 60;
+ second = tmp_vzictime.time_seconds % 60;
+
+ fprintf (fp, "%2i %s %04i\t%2i:%02i:%02i",
+ tmp_vzictime.day_number, months[tmp_vzictime.month],
+ tmp_vzictime.year, hour, minute, second);
+ }
+
+ fprintf (fp, "\t%s", format_tz_offset (vzictime->walloff, FALSE));
+
+ fprintf (fp, "\n");
+}
+
+
+void
+ensure_directory_exists (char *directory)
+{
+ struct stat filestat;
+
+ if (stat (directory, &filestat) != 0) {
+ /* If the directory doesn't exist, try to create it. */
+ if (errno == ENOENT) {
+ if (mkdir (directory, 0777) != 0) {
+ fprintf (stderr, "Can't create directory: %s\n", directory);
+ exit (1);
+ }
+ } else {
+ fprintf (stderr, "Error calling stat() on directory: %s\n", directory);
+ exit (1);
+ }
+ } else if (!S_ISDIR (filestat.st_mode)) {
+ fprintf (stderr, "Can't create directory, already exists: %s\n",
+ directory);
+ exit (1);
+ }
+}
+
+
+static void
+expand_tzid_prefix (void)
+{
+ char *src, *dest;
+ char date_buf[16];
+ char ch1, ch2;
+ time_t t;
+ struct tm *tm;
+
+ /* Get today's date as a string in the format "YYYYMMDD". */
+ t = time (NULL);
+ tm = localtime (&t);
+ sprintf (date_buf, "%4i%02i%02i", tm->tm_year + 1900,
+ tm->tm_mon + 1, tm->tm_mday);
+
+ src = TZIDPrefix;
+ dest = TZIDPrefixExpanded;
+
+ while (ch1 = *src++) {
+
+ /* Look for a '%'. */
+ if (ch1 == '%') {
+ ch2 = *src++;
+
+ if (ch2 == 'D') {
+ /* '%D' gets expanded into the date string. */
+ strcpy (dest, date_buf);
+ dest += strlen (dest);
+ } else if (ch2 == '%') {
+ /* '%%' gets converted into one '%'. */
+ *dest++ = '%';
+ } else {
+ /* Anything else is output as is. */
+ *dest++ = '%';
+ *dest++ = ch2;
+ }
+ } else {
+ *dest++ = ch1;
+ }
+ }
+
+#if 0
+ printf ("TZID : %s\n", TZIDPrefix);
+ printf ("Expanded: %s\n", TZIDPrefixExpanded);
+#endif
+}