/* * 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 * * 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 #include #include #include #include #include #include #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 }