gnome-control-center/panels/datetime/tz.c
Colin Watson 14a2672e28 datetime: Fix handling of Irish timezone on map
The timezone map tries to highlight regions of the world that keep the
same time, approximating this by their non-daylight-savings UTC offset.
There's no reasonable API for this, and it goes wrong in various cases,
such as Irish Standard Time which is legally defined as the country's
summer time with a negative DST offset in winter.

Hardcoding this is unpleasant, but there doesn't seem to be a better
solution, and in any case there's already similar hardcoding implied by
the segmented map images in panels/datetime/data/timezone_*.png.  I've
tried to make it practical to fix other similar disagreements between
the detected offset and the groupings implied by map images, though for
now I've conservatively fixed only the case I'm familiar with.

Fixes: #1341
2021-12-17 14:49:24 +00:00

492 lines
12 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* Generic timezone utilities.
*
* Copyright (C) 2000-2001 Ximian, Inc.
*
* Authors: Hans Petter Jansson <hpj@ximian.com>
*
* Largely based on Michael Fulbright's work on Anaconda.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <math.h>
#include <string.h>
#include <ctype.h>
#include "tz.h"
#include "cc-datetime-resources.h"
/* Forward declarations for private functions */
static float convert_pos (gchar *pos, int digits);
static int compare_country_names (const void *a, const void *b);
static void sort_locations_by_country (GPtrArray *locations);
static gchar * tz_data_file_get (void);
static void load_backward_tz (TzDB *tz_db);
/* ---------------- *
* Public interface *
* ---------------- */
TzDB *
tz_load_db (void)
{
g_autofree gchar *tz_data_file = NULL;
TzDB *tz_db;
FILE *tzfile;
char buf[4096];
tz_data_file = tz_data_file_get ();
if (!tz_data_file) {
g_warning ("Could not get the TimeZone data file name");
return NULL;
}
tzfile = fopen (tz_data_file, "r");
if (!tzfile) {
g_warning ("Could not open *%s*\n", tz_data_file);
return NULL;
}
tz_db = g_new0 (TzDB, 1);
tz_db->locations = g_ptr_array_new ();
while (fgets (buf, sizeof(buf), tzfile))
{
g_auto(GStrv) tmpstrarr = NULL;
g_autofree gchar *latstr = NULL;
g_autofree gchar *lngstr = NULL;
gchar *p;
TzLocation *loc;
if (*buf == '#') continue;
g_strchomp(buf);
tmpstrarr = g_strsplit(buf,"\t", 6);
latstr = g_strdup (tmpstrarr[1]);
p = latstr + 1;
while (*p != '-' && *p != '+') p++;
lngstr = g_strdup (p);
*p = '\0';
loc = g_new0 (TzLocation, 1);
loc->country = g_strdup (tmpstrarr[0]);
loc->zone = g_strdup (tmpstrarr[2]);
loc->latitude = convert_pos (latstr, 2);
loc->longitude = convert_pos (lngstr, 3);
#ifdef __sun
if (tmpstrarr[3] && *tmpstrarr[3] == '-' && tmpstrarr[4])
loc->comment = g_strdup (tmpstrarr[4]);
if (tmpstrarr[3] && *tmpstrarr[3] != '-' && !islower(loc->zone)) {
TzLocation *locgrp;
/* duplicate entry */
locgrp = g_new0 (TzLocation, 1);
locgrp->country = g_strdup (tmpstrarr[0]);
locgrp->zone = g_strdup (tmpstrarr[3]);
locgrp->latitude = convert_pos (latstr, 2);
locgrp->longitude = convert_pos (lngstr, 3);
locgrp->comment = (tmpstrarr[4]) ? g_strdup (tmpstrarr[4]) : NULL;
g_ptr_array_add (tz_db->locations, (gpointer) locgrp);
}
#else
loc->comment = (tmpstrarr[3]) ? g_strdup(tmpstrarr[3]) : NULL;
#endif
g_ptr_array_add (tz_db->locations, (gpointer) loc);
}
fclose (tzfile);
/* now sort by country */
sort_locations_by_country (tz_db->locations);
/* Load up the hashtable of backward links */
load_backward_tz (tz_db);
return tz_db;
}
static void
tz_location_free (TzLocation *loc)
{
g_free (loc->country);
g_free (loc->zone);
g_free (loc->comment);
g_free (loc);
}
void
tz_db_free (TzDB *db)
{
g_ptr_array_foreach (db->locations, (GFunc) tz_location_free, NULL);
g_ptr_array_free (db->locations, TRUE);
g_hash_table_destroy (db->backward);
g_free (db);
}
GPtrArray *
tz_get_locations (TzDB *db)
{
return db->locations;
}
gchar *
tz_location_get_country (TzLocation *loc)
{
return loc->country;
}
gchar *
tz_location_get_zone (TzLocation *loc)
{
return loc->zone;
}
gchar *
tz_location_get_comment (TzLocation *loc)
{
return loc->comment;
}
void
tz_location_get_position (TzLocation *loc, double *longitude, double *latitude)
{
*longitude = loc->longitude;
*latitude = loc->latitude;
}
/* For timezone map display purposes, we try to highlight regions of the
* world that keep the same time. There is no reasonable API to discover
* this; at the moment we just group timezones by their non-daylight-savings
* UTC offset and hope that's good enough. However, in some cases that
* produces confusing results. For example, Irish Standard Time is legally
* defined as the country's summer time, with a negative DST offset in
* winter; but this results in the same observed clock times as countries
* that observe Western European (Summer) Time, not those that observe
* Central European (Summer) Time, so we should group Ireland with the
* former, matching the grouping implied by data/timezone_*.png.
*
* This is something of a hack, and there remain other problems with
* timezone grouping: for example, grouping timezones north and south of the
* equator together where DST is observed at different times of the year is
* dubious.
*/
struct {
const char *zone;
gint offset;
} base_offset_overrides[] = {
{ "Europe/Dublin", 0 },
};
glong
tz_location_get_base_utc_offset (TzLocation *loc)
{
g_autoptr(TzInfo) tz_info = NULL;
glong offset;
guint i;
tz_info = tz_info_from_location (loc);
offset = tz_info->utc_offset + (tz_info->daylight ? -3600 : 0);
for (i = 0; i < G_N_ELEMENTS (base_offset_overrides); i++) {
if (g_str_equal (loc->zone, base_offset_overrides[i].zone)) {
offset = base_offset_overrides[i].offset;
break;
}
}
return offset;
}
TzInfo *
tz_info_from_location (TzLocation *loc)
{
TzInfo *tzinfo;
time_t curtime;
struct tm *curzone;
g_autofree gchar *tz_env_value = NULL;
g_return_val_if_fail (loc != NULL, NULL);
g_return_val_if_fail (loc->zone != NULL, NULL);
tz_env_value = g_strdup (getenv ("TZ"));
setenv ("TZ", loc->zone, 1);
#if 0
tzset ();
#endif
tzinfo = g_new0 (TzInfo, 1);
curtime = time (NULL);
curzone = localtime (&curtime);
#ifndef __sun
tzinfo->tzname = g_strdup (curzone->tm_zone);
tzinfo->utc_offset = curzone->tm_gmtoff;
#else
tzinfo->tzname = NULL;
tzinfo->utc_offset = 0;
#endif
tzinfo->daylight = curzone->tm_isdst;
if (tz_env_value)
setenv ("TZ", tz_env_value, 1);
else
unsetenv ("TZ");
return tzinfo;
}
void
tz_info_free (TzInfo *tzinfo)
{
g_return_if_fail (tzinfo != NULL);
if (tzinfo->tzname) g_free (tzinfo->tzname);
g_free (tzinfo);
}
struct {
const char *orig;
const char *dest;
} aliases[] = {
{ "Asia/Istanbul", "Europe/Istanbul" }, /* Istanbul is in both Europe and Asia */
{ "Europe/Nicosia", "Asia/Nicosia" }, /* Ditto */
{ "EET", "Europe/Istanbul" }, /* Same tz as the 2 above */
{ "HST", "Pacific/Honolulu" },
{ "WET", "Europe/Brussels" }, /* Other name for the mainland Europe tz */
{ "CET", "Europe/Brussels" }, /* ditto */
{ "MET", "Europe/Brussels" },
{ "Etc/Zulu", "Etc/GMT" },
{ "Etc/UTC", "Etc/GMT" },
{ "GMT", "Etc/GMT" },
{ "Greenwich", "Etc/GMT" },
{ "Etc/UCT", "Etc/GMT" },
{ "Etc/GMT0", "Etc/GMT" },
{ "Etc/GMT+0", "Etc/GMT" },
{ "Etc/GMT-0", "Etc/GMT" },
{ "Etc/Universal", "Etc/GMT" },
{ "PST8PDT", "America/Los_Angeles" }, /* Other name for the Atlantic tz */
{ "EST", "America/New_York" }, /* Other name for the Eastern tz */
{ "EST5EDT", "America/New_York" }, /* ditto */
{ "CST6CDT", "America/Chicago" }, /* Other name for the Central tz */
{ "MST", "America/Denver" }, /* Other name for the mountain tz */
{ "MST7MDT", "America/Denver" }, /* ditto */
};
static gboolean
compare_timezones (const char *a,
const char *b)
{
if (g_str_equal (a, b))
return TRUE;
if (strchr (b, '/') == NULL) {
g_autofree gchar *prefixed = NULL;
prefixed = g_strdup_printf ("/%s", b);
if (g_str_has_suffix (a, prefixed))
return TRUE;
}
return FALSE;
}
char *
tz_info_get_clean_name (TzDB *tz_db,
const char *tz)
{
char *ret;
const char *timezone;
guint i;
gboolean replaced;
/* Remove useless prefixes */
if (g_str_has_prefix (tz, "right/"))
tz = tz + strlen ("right/");
else if (g_str_has_prefix (tz, "posix/"))
tz = tz + strlen ("posix/");
/* Here start the crazies */
replaced = FALSE;
for (i = 0; i < G_N_ELEMENTS (aliases); i++) {
if (compare_timezones (tz, aliases[i].orig)) {
replaced = TRUE;
timezone = aliases[i].dest;
break;
}
}
/* Try again! */
if (!replaced) {
/* Ignore crazy solar times from the '80s */
if (g_str_has_prefix (tz, "Asia/Riyadh") ||
g_str_has_prefix (tz, "Mideast/Riyadh")) {
timezone = "Asia/Riyadh";
replaced = TRUE;
}
}
if (!replaced)
timezone = tz;
ret = g_hash_table_lookup (tz_db->backward, timezone);
if (ret == NULL)
return g_strdup (timezone);
return g_strdup (ret);
}
/* ----------------- *
* Private functions *
* ----------------- */
static gchar *
tz_data_file_get (void)
{
gchar *file;
file = g_strdup (TZ_DATA_FILE);
return file;
}
static float
convert_pos (gchar *pos, int digits)
{
gchar whole[10];
gchar *fraction;
gint i;
float t1, t2;
if (!pos || strlen(pos) < 4 || digits > 9) return 0.0;
for (i = 0; i < digits + 1; i++) whole[i] = pos[i];
whole[i] = '\0';
fraction = pos + digits + 1;
t1 = g_strtod (whole, NULL);
t2 = g_strtod (fraction, NULL);
if (t1 >= 0.0) return t1 + t2/pow (10.0, strlen(fraction));
else return t1 - t2/pow (10.0, strlen(fraction));
}
#if 0
/* Currently not working */
static void
free_tzdata (TzLocation *tz)
{
if (tz->country)
g_free(tz->country);
if (tz->zone)
g_free(tz->zone);
if (tz->comment)
g_free(tz->comment);
g_free(tz);
}
#endif
static int
compare_country_names (const void *a, const void *b)
{
const TzLocation *tza = * (TzLocation **) a;
const TzLocation *tzb = * (TzLocation **) b;
return strcmp (tza->zone, tzb->zone);
}
static void
sort_locations_by_country (GPtrArray *locations)
{
qsort (locations->pdata, locations->len, sizeof (gpointer),
compare_country_names);
}
static void
load_backward_tz (TzDB *tz_db)
{
g_auto(GStrv) lines = NULL;
g_autoptr(GBytes) bytes = NULL;
const char *contents;
guint i;
tz_db->backward = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
bytes = g_resources_lookup_data ("/org/gnome/control-center/datetime/backward",
G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
contents = (const char *) g_bytes_get_data (bytes, NULL);
lines = g_strsplit (contents, "\n", -1);
for (i = 0; lines[i] != NULL; i++)
{
g_auto(GStrv) items = NULL;
guint j;
char *real, *alias;
if (g_ascii_strncasecmp (lines[i], "Link\t", 5) != 0)
continue;
items = g_strsplit (lines[i], "\t", -1);
real = NULL;
alias = NULL;
/* Skip the "Link<tab>" part */
for (j = 1; items[j] != NULL; j++)
{
if (items[j][0] == '\0')
continue;
if (real == NULL)
{
real = items[j];
continue;
}
alias = items[j];
break;
}
if (real == NULL || alias == NULL)
g_warning ("Could not parse line: %s", lines[i]);
/* We don't need more than one name for it */
if (g_str_equal (real, "Etc/UTC") ||
g_str_equal (real, "Etc/UCT"))
real = "Etc/GMT";
g_hash_table_insert (tz_db->backward, g_strdup (alias), g_strdup (real));
}
}