You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2326 lines
66 KiB
2326 lines
66 KiB
/*
|
|
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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
|
|
}
|