freeswitch/libs/spandsp/src/timezone.c

821 lines
25 KiB
C

/*
* SpanDSP - a series of DSP components for telephony
*
* timezone.c - Timezone handling for time interpretation
*
* Written by Steve Underwood <steveu@coppice.org>
*
* Copyright (C) 2010 Steve Underwood
*
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 2.1,
* as published by the Free Software Foundation.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*! \file */
/* Timezone processing might not seem like a DSP activity, but getting the headers
right on FAXes demands it. We need to handle multiple time zones within a process,
for FAXes related to different parts of the globe, so the system timezone handling
is not adequate. */
/* This timezone handling is derived from public domain software by Arthur David Olson
<arthur_david_olson@nih.gov> which you may download from ftp://elsie.nci.nih.gov/pub
at the time of writing. */
#if defined(HAVE_CONFIG_H)
#include "config.h"
#endif
#include <stdlib.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#if defined(HAVE_STDBOOL_H)
#include <stdbool.h>
#else
#include "spandsp/stdbool.h"
#endif
#include <assert.h>
#include "spandsp/telephony.h"
#include "spandsp/alloc.h"
#include "spandsp/timezone.h"
#include "spandsp/private/timezone.h"
#define SECS_PER_MIN 60
#define MINS_PER_HOUR 60
#define HOURS_PER_DAY 24
#define DAYS_PER_WEEK 7
#define DAYS_PER_NON_LEAP_YEAR 365
#define DAYS_PER_LEAP_YEAR 366
#define SECS_PER_HOUR (SECS_PER_MIN*MINS_PER_HOUR)
#define SECS_PER_DAY ((long int) SECS_PER_HOUR*HOURS_PER_DAY)
#define MONTHS_PER_YEAR 12
#define TM_YEAR_BASE 1900
#define EPOCH_YEAR 1970
#define EPOCH_WDAY TM_THURSDAY
#define isleap(y) (((y)%4) == 0 && (((y)%100) != 0 || ((y)%400) == 0))
#define isleap_sum(a, b) isleap((a)%400 + (b)%400)
/* Unlike <ctype.h>'s isdigit, this also works if c < 0 | c > UCHAR_MAX. */
#define is_digit(c) ((unsigned int) (c) - '0' <= 9)
#define TZ_DEF_RULE_STRING ",M4.1.0,M10.5.0"
#define JULIAN_DAY 0 /* Jn - Julian day */
#define DAY_OF_YEAR 1 /* n - day of year */
#define MONTH_NTH_DAY_OF_WEEK 2 /* Mm.n.d - month, week, day of week */
static const char wildabbr[] = " ";
static const char gmt[] = "GMT";
struct tz_rule_s
{
int r_type; /* Type of rule--see below */
int r_day; /* Day number of rule */
int r_week; /* Week number of rule */
int r_mon; /* Month number of rule */
long int r_time; /* Transition time of rule */
};
static const int mon_lengths[2][MONTHS_PER_YEAR] =
{
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
static const int year_lengths[2] =
{
DAYS_PER_NON_LEAP_YEAR,
DAYS_PER_LEAP_YEAR
};
static int add_with_overflow_detection(int *number, int delta)
{
/* This needs to be considered volatile, or clever optimisation destroys
the effect of the the rollover detection logic */
volatile int last_number;
last_number = *number;
*number += delta;
return (*number < last_number) != (delta < 0);
}
/*- End of function --------------------------------------------------------*/
static void set_tzname(tz_t *tz)
{
struct tz_state_s *sp;
const struct tz_ttinfo_s *ttisp;
int i;
sp = &tz->state;
tz->tzname[0] = wildabbr;
tz->tzname[1] = wildabbr;
for (i = 0; i < sp->typecnt; i++)
{
ttisp = &sp->ttis[i];
tz->tzname[ttisp->isdst] = &sp->chars[ttisp->abbrind];
}
for (i = 0; i < sp->timecnt; i++)
{
ttisp = &sp->ttis[sp->types[i]];
tz->tzname[ttisp->isdst] = &sp->chars[ttisp->abbrind];
}
}
/*- End of function --------------------------------------------------------*/
/* Return the number of leap years through the end of the given year
where, to make the math easy, the answer for year zero is defined as zero. */
static int leaps_thru_end_of(const int y)
{
return (y >= 0) ? (y/4 - y/100 + y/400) : -(leaps_thru_end_of(-(y + 1)) + 1);
}
/*- End of function --------------------------------------------------------*/
static struct tm *time_sub(const time_t * const timep, const long int offset, const struct tz_state_s * const sp, struct tm * const tmp)
{
const struct tz_lsinfo_s *lp;
time_t tdays;
const int *ip;
int32_t corr;
int32_t seconds;
int32_t rem;
int idays;
int y;
int hit;
int i;
int newy;
time_t tdelta;
int idelta;
int leapdays;
corr = 0;
hit = 0;
i = sp->leapcnt;
while (--i >= 0)
{
lp = &sp->lsis[i];
if (*timep >= lp->trans)
{
if (*timep == lp->trans)
{
hit = ((i == 0 && lp->corr > 0) || lp->corr > sp->lsis[i - 1].corr);
if (hit)
{
while (i > 0
&&
sp->lsis[i].trans == sp->lsis[i - 1].trans + 1
&&
sp->lsis[i].corr == sp->lsis[i - 1].corr + 1)
{
hit++;
--i;
}
}
}
corr = lp->corr;
break;
}
}
y = EPOCH_YEAR;
tdays = *timep/SECS_PER_DAY;
rem = *timep - tdays*SECS_PER_DAY;
while (tdays < 0 || tdays >= year_lengths[isleap(y)])
{
tdelta = tdays / DAYS_PER_LEAP_YEAR;
idelta = tdelta;
if (tdelta - idelta >= 1 || idelta - tdelta >= 1)
return NULL;
if (idelta == 0)
idelta = (tdays < 0) ? -1 : 1;
newy = y;
if (add_with_overflow_detection(&newy, idelta))
return NULL;
leapdays = leaps_thru_end_of(newy - 1) - leaps_thru_end_of(y - 1);
tdays -= ((time_t) newy - y)*DAYS_PER_NON_LEAP_YEAR;
tdays -= leapdays;
y = newy;
}
seconds = tdays*SECS_PER_DAY;
tdays = seconds/SECS_PER_DAY;
rem += seconds - tdays*SECS_PER_DAY;
/* Given the range, we can now fearlessly cast... */
idays = tdays;
rem += (offset - corr);
while (rem < 0)
{
rem += SECS_PER_DAY;
idays--;
}
while (rem >= SECS_PER_DAY)
{
rem -= SECS_PER_DAY;
idays++;
}
while (idays < 0)
{
if (add_with_overflow_detection(&y, -1))
return NULL;
idays += year_lengths[isleap(y)];
}
while (idays >= year_lengths[isleap(y)])
{
idays -= year_lengths[isleap(y)];
if (add_with_overflow_detection(&y, 1))
return NULL;
}
tmp->tm_year = y;
if (add_with_overflow_detection(&tmp->tm_year, -TM_YEAR_BASE))
return NULL;
tmp->tm_yday = idays;
/* The "extra" mods below avoid overflow problems. */
tmp->tm_wday = EPOCH_WDAY
+ ((y - EPOCH_YEAR) % DAYS_PER_WEEK)*(DAYS_PER_NON_LEAP_YEAR % DAYS_PER_WEEK)
+ leaps_thru_end_of(y - 1)
- leaps_thru_end_of(EPOCH_YEAR - 1)
+ idays;
tmp->tm_wday %= DAYS_PER_WEEK;
if (tmp->tm_wday < 0)
tmp->tm_wday += DAYS_PER_WEEK;
tmp->tm_hour = (int) (rem/SECS_PER_HOUR);
rem %= SECS_PER_HOUR;
tmp->tm_min = (int) (rem/SECS_PER_MIN);
/* A positive leap second requires a special
* representation. This uses "... ??:59:60" et seq. */
tmp->tm_sec = (int) (rem%SECS_PER_MIN) + hit;
ip = mon_lengths[isleap(y)];
for (tmp->tm_mon = 0; idays >= ip[tmp->tm_mon]; (tmp->tm_mon)++)
idays -= ip[tmp->tm_mon];
tmp->tm_mday = (int) (idays + 1);
tmp->tm_isdst = 0;
return tmp;
}
/*- End of function --------------------------------------------------------*/
/* Given a pointer into a time zone string, scan until a character that is not
* a valid character in a zone name is found. Return a pointer to that
* character. */
static const char *get_tzname(const char *strp)
{
char c;
while ((c = *strp) != '\0' && !is_digit(c) && c != ',' && c != '-' && c != '+')
strp++;
return strp;
}
/*- End of function --------------------------------------------------------*/
/* Given a pointer into a time zone string, extract a number from that string.
* Check that the number is within a specified range; if it is not, return
* NULL.
* Otherwise, return a pointer to the first character not part of the number. */
static const char *get_num(const char *strp, int * const nump, const int min, const int max)
{
char c;
int num;
if (strp == NULL || !is_digit(c = *strp))
return NULL;
num = 0;
do
{
num = num*10 + (c - '0');
if (num > max)
return NULL; /* Illegal value */
c = *++strp;
}
while (is_digit(c));
if (num < min)
return NULL; /* Illegal value */
*nump = num;
return strp;
}
/*- End of function --------------------------------------------------------*/
/* Given a pointer into a time zone string, extract a number of seconds,
* in hh[:mm[:ss]] form, from the string.
* If any error occurs, return NULL.
* Otherwise, return a pointer to the first character not part of the number
* of seconds. */
static const char *get_secs(const char *strp, long int * const secsp)
{
int num;
/* HOURS_PER_DAY*DAYS_PER_WEEK - 1 allows quasi-Posix rules like
* "M10.4.6/26", which does not conform to Posix,
* but which specifies the equivalent of
* "02:00 on the first Sunday on or after 23 Oct". */
strp = get_num(strp, &num, 0, HOURS_PER_DAY*DAYS_PER_WEEK - 1);
if (strp == NULL)
return NULL;
*secsp = num*(long int) SECS_PER_HOUR;
if (*strp == ':')
{
strp = get_num(strp + 1, &num, 0, MINS_PER_HOUR - 1);
if (strp == NULL)
return NULL;
*secsp += num*SECS_PER_MIN;
if (*strp == ':')
{
/* SECS_PER_MIN allows for leap seconds. */
strp = get_num(strp + 1, &num, 0, SECS_PER_MIN);
if (strp == NULL)
return NULL;
*secsp += num;
}
}
return strp;
}
/*- End of function --------------------------------------------------------*/
/* Given a pointer into a time zone string, extract an offset, in
* [+-]hh[:mm[:ss]] form, from the string.
* If any error occurs, return NULL.
* Otherwise, return a pointer to the first character not part of the time. */
static const char *get_offset(const char *strp, long int * const offsetp)
{
int neg = 0;
if (*strp == '-')
{
neg = 1;
strp++;
}
else if (*strp == '+')
{
strp++;
}
strp = get_secs(strp, offsetp);
if (strp == NULL)
return NULL; /* Illegal time */
if (neg)
*offsetp = -*offsetp;
return strp;
}
/*- End of function --------------------------------------------------------*/
/* Given a pointer into a time zone string, extract a rule in the form
* date[/time]. See POSIX section 8 for the format of "date" and "time".
* If a valid rule is not found, return NULL.
* Otherwise, return a pointer to the first character not part of the rule. */
static const char *get_rule(const char *strp, struct tz_rule_s * const rulep)
{
if (*strp == 'J')
{
/* Julian day. */
rulep->r_type = JULIAN_DAY;
strp = get_num(strp + 1, &rulep->r_day, 1, DAYS_PER_NON_LEAP_YEAR);
}
else if (*strp == 'M')
{
/* Month, week, day. */
rulep->r_type = MONTH_NTH_DAY_OF_WEEK;
strp = get_num(strp + 1, &rulep->r_mon, 1, MONTHS_PER_YEAR);
if (strp == NULL || *strp++ != '.')
return NULL;
strp = get_num(strp, &rulep->r_week, 1, 5);
if (strp == NULL || *strp++ != '.')
return NULL;
strp = get_num(strp, &rulep->r_day, 0, DAYS_PER_WEEK - 1);
}
else if (is_digit(*strp))
{
/* Day of the year. */
rulep->r_type = DAY_OF_YEAR;
strp = get_num(strp, &rulep->r_day, 0, DAYS_PER_LEAP_YEAR - 1);
}
else
{
/* Invalid format */
return NULL;
}
if (strp == NULL)
return NULL;
if (*strp == '/')
{
/* Time specified. */
strp = get_secs(strp + 1, &rulep->r_time);
}
else
{
/* Default = 2:00:00 */
rulep->r_time = 2*SECS_PER_HOUR;
}
return strp;
}
/*- End of function --------------------------------------------------------*/
/* Given the Epoch-relative time of January 1, 00:00:00 UTC, in a year, the
* year, a rule, and the offset from UTC at the time that rule takes effect,
* calculate the Epoch-relative time that rule takes effect. */
static time_t trans_time(const time_t janfirst, const int year, const struct tz_rule_s * const rulep, const long int offset)
{
int leapyear;
time_t value;
int i;
int d;
int m1;
int yy0;
int yy1;
int yy2;
int dow;
value = 0;
leapyear = isleap(year);
switch (rulep->r_type)
{
case JULIAN_DAY:
/* Jn - Julian day, 1 == January 1, 60 == March 1 even in leap
* years.
* In non-leap years, or if the day number is 59 or less, just
* add SECS_PER_DAY times the day number-1 to the time of
* January 1, midnight, to get the day. */
value = janfirst + (rulep->r_day - 1)*SECS_PER_DAY;
if (leapyear && rulep->r_day >= 60)
value += SECS_PER_DAY;
break;
case DAY_OF_YEAR:
/* n - day of year.
* Just add SECS_PER_DAY times the day number to the time of
* January 1, midnight, to get the day. */
value = janfirst + rulep->r_day * SECS_PER_DAY;
break;
case MONTH_NTH_DAY_OF_WEEK:
/* Mm.n.d - nth "dth day" of month m. */
value = janfirst;
for (i = 0; i < rulep->r_mon - 1; i++)
value += mon_lengths[leapyear][i]*SECS_PER_DAY;
/* Use Zeller's Congruence to get day-of-week of first day of month. */
m1 = (rulep->r_mon + 9)%12 + 1;
yy0 = (rulep->r_mon <= 2) ? (year - 1) : year;
yy1 = yy0/100;
yy2 = yy0%100;
dow = ((26*m1 - 2)/10 + 1 + yy2 + yy2/4 + yy1/4 - 2*yy1)%7;
if (dow < 0)
dow += DAYS_PER_WEEK;
/* "dow" is the day-of-week of the first day of the month. Get
* the day-of-month (zero-origin) of the first "dow" day of the
* month. */
d = rulep->r_day - dow;
if (d < 0)
d += DAYS_PER_WEEK;
for (i = 1; i < rulep->r_week; i++)
{
if (d + DAYS_PER_WEEK >= mon_lengths[leapyear][rulep->r_mon - 1])
break;
d += DAYS_PER_WEEK;
}
/* "d" is the day-of-month (zero-origin) of the day we want. */
value += d*SECS_PER_DAY;
break;
}
/* "value" is the Epoch-relative time of 00:00:00 UTC on the day in
* question. To get the Epoch-relative time of the specified local
* time on that day, add the transition time and the current offset
* from UTC. */
return value + rulep->r_time + offset;
}
/*- End of function --------------------------------------------------------*/
/* Given a POSIX section 8-style TZ string, fill in the rule tables as
appropriate. */
static int tzparse(const char *name, struct tz_state_s * const sp, const int lastditch)
{
const char *stdname;
const char *dstname;
size_t stdlen;
size_t dstlen;
long int stdoffset;
long int dstoffset;
long int theirstdoffset;
long int theirdstoffset;
long int theiroffset;
unsigned char *typep;
char *cp;
int load_result;
int isdst;
int i;
int j;
int year;
struct tz_rule_s start;
struct tz_rule_s end;
time_t *atp;
time_t janfirst;
time_t starttime;
time_t endtime;
dstname = NULL;
stdname = name;
if (lastditch)
{
stdlen = strlen(name); /* Length of standard zone name */
name += stdlen;
if (stdlen >= sizeof(sp->chars))
stdlen = sizeof(sp->chars) - 1;
stdoffset = 0;
}
else
{
name = get_tzname(name);
stdlen = name - stdname;
if (stdlen < 3)
return -1;
if (*name == '\0')
return -1;
name = get_offset(name, &stdoffset);
if (name == NULL)
return -1;
}
load_result = -1;
if (load_result != 0)
sp->leapcnt = 0; /* So, we're off a little */
if (*name != '\0')
{
dstname = name;
name = get_tzname(name);
dstlen = name - dstname; /* Length of DST zone name */
if (dstlen < 3)
return -1;
if (*name != '\0' && *name != ',' && *name != ';')
{
if ((name = get_offset(name, &dstoffset)) == NULL)
return -1;
}
else
{
dstoffset = stdoffset - SECS_PER_HOUR;
}
if (*name == '\0' && load_result != 0)
name = TZ_DEF_RULE_STRING;
if (*name == ',' || *name == ';')
{
if ((name = get_rule(name + 1, &start)) == NULL)
return -1;
if (*name++ != ',')
return -1;
if ((name = get_rule(name, &end)) == NULL)
return -1;
if (*name != '\0')
return -1;
sp->typecnt = 2; /* Standard time and DST */
/* Two transitions per year, from EPOCH_YEAR to 2037. */
sp->timecnt = 2*(2037 - EPOCH_YEAR + 1);
if (sp->timecnt > TZ_MAX_TIMES)
return -1;
sp->ttis[0].gmtoff = -dstoffset;
sp->ttis[0].isdst = 1;
sp->ttis[0].abbrind = stdlen + 1;
sp->ttis[1].gmtoff = -stdoffset;
sp->ttis[1].isdst = 0;
sp->ttis[1].abbrind = 0;
atp = sp->ats;
typep = sp->types;
janfirst = 0;
for (year = EPOCH_YEAR; year <= 2037; year++)
{
starttime = trans_time(janfirst, year, &start, stdoffset);
endtime = trans_time(janfirst, year, &end, dstoffset);
if (starttime > endtime)
{
*atp++ = endtime;
*typep++ = 1; /* DST ends */
*atp++ = starttime;
*typep++ = 0; /* DST begins */
}
else
{
*atp++ = starttime;
*typep++ = 0; /* DST begins */
*atp++ = endtime;
*typep++ = 1; /* DST ends */
}
janfirst += year_lengths[isleap(year)]*SECS_PER_DAY;
}
}
else
{
if (*name != '\0')
return -1;
/* Initial values of theirstdoffset and theirdstoffset. */
theirstdoffset = 0;
for (i = 0; i < sp->timecnt; i++)
{
j = sp->types[i];
if (!sp->ttis[j].isdst)
{
theirstdoffset = -sp->ttis[j].gmtoff;
break;
}
}
theirdstoffset = 0;
for (i = 0; i < sp->timecnt; i++)
{
j = sp->types[i];
if (sp->ttis[j].isdst)
{
theirdstoffset = -sp->ttis[j].gmtoff;
break;
}
}
/* Initially we're assumed to be in standard time. */
isdst = false;
/* Now juggle transition times and types tracking offsets as you do. */
for (i = 0; i < sp->timecnt; i++)
{
j = sp->types[i];
sp->types[i] = sp->ttis[j].isdst;
if (sp->ttis[j].ttisgmt)
{
/* No adjustment to transition time */
}
else
{
/* If summer time is in effect, and the
* transition time was not specified as
* standard time, add the summer time
* offset to the transition time;
* otherwise, add the standard time
* offset to the transition time. */
/* Transitions from DST to DDST
* will effectively disappear since
* POSIX provides for only one DST
* offset. */
if (isdst && !sp->ttis[j].ttisstd)
sp->ats[i] += (dstoffset - theirdstoffset);
else
sp->ats[i] += (stdoffset - theirstdoffset);
}
theiroffset = -sp->ttis[j].gmtoff;
if (sp->ttis[j].isdst)
theirdstoffset = theiroffset;
else
theirstdoffset = theiroffset;
}
/* Finally, fill in ttis. ttisstd and ttisgmt need not be handled. */
sp->ttis[0].gmtoff = -stdoffset;
sp->ttis[0].isdst = false;
sp->ttis[0].abbrind = 0;
sp->ttis[1].gmtoff = -dstoffset;
sp->ttis[1].isdst = true;
sp->ttis[1].abbrind = stdlen + 1;
sp->typecnt = 2;
}
}
else
{
dstlen = 0;
sp->typecnt = 1; /* Only standard time */
sp->timecnt = 0;
sp->ttis[0].gmtoff = -stdoffset;
sp->ttis[0].isdst = 0;
sp->ttis[0].abbrind = 0;
}
sp->charcnt = stdlen + 1;
if (dstlen != 0)
sp->charcnt += dstlen + 1;
if ((size_t) sp->charcnt > sizeof(sp->chars))
return -1;
cp = sp->chars;
strncpy(cp, stdname, stdlen);
cp += stdlen;
*cp++ = '\0';
if (dstlen != 0)
{
strncpy(cp, dstname, dstlen);
cp[dstlen] = '\0';
}
return 0;
}
/*- End of function --------------------------------------------------------*/
static void tz_set(tz_t *tz, const char *tzstring)
{
const char *name = "";
struct tz_state_s *lclptr = &tz->state;
if (tzstring)
name = tzstring;
/* See if we are already set OK */
if (tz->lcl_is_set > 0 && strcmp(tz->lcl_tzname, name) == 0)
return;
tz->lcl_is_set = strlen(name) < sizeof(tz->lcl_tzname);
if (tz->lcl_is_set)
strcpy(tz->lcl_tzname, name);
if (name[0] == '\0')
{
/* User wants it fast rather than right, so, we're off a little. */
lclptr->leapcnt = 0;
lclptr->timecnt = 0;
lclptr->typecnt = 0;
lclptr->ttis[0].isdst = 0;
lclptr->ttis[0].gmtoff = 0;
lclptr->ttis[0].abbrind = 0;
strcpy(lclptr->chars, gmt);
}
else if (name[0] == ':' || tzparse(name, lclptr, false) != 0)
{
tzparse(gmt, lclptr, true);
}
set_tzname(tz);
}
/*- End of function --------------------------------------------------------*/
SPAN_DECLARE(int) tz_localtime(tz_t *tz, struct tm *tmp, time_t t)
{
struct tz_state_s *sp;
const struct tz_ttinfo_s *ttisp;
int i;
sp = &tz->state;
if (sp->timecnt == 0 || t < sp->ats[0])
{
i = 0;
while (sp->ttis[i].isdst)
{
if (++i >= sp->typecnt)
{
i = 0;
break;
}
}
}
else
{
for (i = 1; i < sp->timecnt; i++)
{
if (t < sp->ats[i])
break;
}
i = (int) sp->types[i - 1];
}
ttisp = &sp->ttis[i];
time_sub(&t, ttisp->gmtoff, sp, tmp);
tmp->tm_isdst = ttisp->isdst;
tz->tzname[tmp->tm_isdst] = &sp->chars[ttisp->abbrind];
return 0;
}
/*- End of function --------------------------------------------------------*/
SPAN_DECLARE(const char *) tz_tzname(tz_t *tz, int isdst)
{
return tz->tzname[(!isdst) ? 0 : 1];
}
/*- End of function --------------------------------------------------------*/
SPAN_DECLARE(tz_t *) tz_init(tz_t *tz, const char *tzstring)
{
if (tz == NULL)
{
if ((tz = (tz_t *) span_alloc(sizeof(*tz))) == NULL)
return NULL;
}
memset(tz, 0, sizeof(*tz));
tz->tzname[0] =
tz->tzname[1] = wildabbr;
tz_set(tz, tzstring);
return tz;
}
/*- End of function --------------------------------------------------------*/
SPAN_DECLARE(int) tz_release(tz_t *tz)
{
return 0;
}
/*- End of function --------------------------------------------------------*/
SPAN_DECLARE(int) tz_free(tz_t *tz)
{
if (tz)
span_free(tz);
return 0;
}
/*- End of function --------------------------------------------------------*/
/*- End of file ------------------------------------------------------------*/