1 /**
2  * @file date.c date formatting routines
3  *
4  * Copyright (C) 2008-2011  Lars Windolf <lars.windolf@gmx.de>
5  * Copyright (C) 2004-2006  Nathan J. Conrad <t98502@users.sourceforge.net>
6  *
7  * The date formatting was reused from the Evolution code base
8  *
9  *    Author: Chris Lahey <clahey@ximian.com
10  *
11  *    Copyright 2001, Ximian, Inc.
12  *
13  * parts of the RFC822 timezone decoding were reused from the gmime source
14  *
15  *    Authors: Michael Zucchi <notzed@helixcode.com>
16  *             Jeffrey Stedfast <fejj@helixcode.com>
17  *
18  *    Copyright 2000 Helix Code, Inc. (www.helixcode.com)
19  *
20  * This program is free software; you can redistribute it and/or modify
21  * it under the terms of the GNU General Public License as published by
22  * the Free Software Foundation; either version 2 of the License, or
23  * (at your option) any later version.
24  *
25  * This program is distributed in the hope that it will be useful,
26  * but WITHOUT ANY WARRANTY; without even the implied warranty of
27  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28  * GNU General Public License for more details.
29  *
30  * You should have received a copy of the GNU General Public License
31  * along with this program; if not, write to the Free Software
32  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
33  */
34 
35 #include "date.h"
36 
37 #include <string.h>
38 
39 #include "common.h"
40 #include "debug.h"
41 
42 /* date formatting methods */
43 
44 /**
45  * Originally from Evolution e-util.c
46  *
47  * Function to do a last minute fixup of the AM/PM stuff if the locale
48  * and gettext haven't done it right. Most English speaking countries
49  * except the USA use the 24 hour clock (UK, Australia etc). However
50  * since they are English nobody bothers to write a language
51  * translation (gettext) file. So the locale turns off the AM/PM, but
52  * gettext does not turn on the 24 hour clock. Leaving a mess.
53  *
54  * This routine checks if AM/PM are defined in the locale, if not it
55  * forces the use of the 24 hour clock.
56  *
57  * The function itself is a front end on strftime and takes exactly
58  * the same arguments.
59  *
60  * TODO: Actually remove the '%p' from the fixed up string so that
61  * there isn't a stray space.
62  **/
63 
64 static gchar *
e_utf8_strftime_fix_am_pm(const char * fmt,GDateTime * tm)65 e_utf8_strftime_fix_am_pm (const char *fmt, GDateTime *tm)
66 {
67 	gchar *buf;
68 	char *sp;
69 	char *ffmt;
70 	gchar *datestr;
71 
72 	if (g_strstr_len (fmt, -1, "%p")==NULL && g_strstr_len (fmt, -1, "%P")==NULL) {
73 		/* No AM/PM involved - can use the fmt string directly */
74 		datestr = g_date_time_format (tm, fmt);
75 	} else {
76 		/* Get the AM/PM symbol from the locale */
77 		buf = g_date_time_format (tm, "%p");
78 
79 		if (buf && buf[0]) {
80 			/**
81 			 * AM/PM have been defined in the locale
82 			 * so we can use the fmt string directly
83 			 **/
84 			datestr = g_date_time_format (tm, fmt);
85 		} else {
86 			/**
87 			 * No AM/PM defined by locale
88 			 * must change to 24 hour clock
89 			 **/
90 			ffmt=g_strdup(fmt);
91 			for (sp=ffmt; (sp = g_strstr_len (sp, -1, "%l")); sp++) {
92 				/**
93 				 * Maybe this should be 'k', but I have never
94 				 * seen a 24 clock actually use that format
95 				 **/
96 				sp[1]='H';
97 			}
98 			for (sp=ffmt; (sp=g_strstr_len (sp, -1, "%I")); sp++) {
99 				sp[1]='H';
100 			}
101 			datestr = g_date_time_format (tm, ffmt);
102 			g_free(ffmt);
103 		}
104 		g_free (buf);
105 	}
106 	return datestr;
107 }
108 
109 /* This function is originally from the Evolution 2.6.2 code (e-cell-date.c) */
110 static gchar *
date_format_nice(gint64 date)111 date_format_nice (gint64 date)
112 {
113 	GDateTime *then, *now, *yesterday;
114 	gchar *temp, *buf;
115 	gboolean done = FALSE;
116 
117 	then = g_date_time_new_from_unix_local (date);
118 	now = g_date_time_new_now_local ();
119 
120 	if ((date == 0) || (then == NULL)) {
121 		return g_strdup ("");
122 	}
123 
124 /*	if (nowdate - date < 60 * 60 * 8 && nowdate > date) {
125 		e_utf8_strftime_fix_am_pm (buf, TIMESTRLEN, _("%l:%M %p"), &then);
126 		done = TRUE;
127 	}*/
128 
129 	if (!done) {
130 		if (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (now) &&
131 		    g_date_time_get_year (then) == g_date_time_get_year (now)) {
132 		    	/* translation hint: date format for today, reorder format codes as necessary */
133 			buf = e_utf8_strftime_fix_am_pm (_("Today %l:%M %p"), then);
134 			done = TRUE;
135 		}
136 	}
137 	if (!done) {
138 		yesterday = g_date_time_add_days (now, -1);
139 		if (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (yesterday) &&
140 		    g_date_time_get_year (then) == g_date_time_get_year (yesterday)) {
141 		    	/* translation hint: date format for yesterday, reorder format codes as necessary */
142 			buf = e_utf8_strftime_fix_am_pm (_("Yesterday %l:%M %p"), then);
143 			done = TRUE;
144 		}
145 		g_date_time_unref (yesterday);
146 	}
147 	if (!done) {
148 		yesterday = g_date_time_add_days (now, -6);
149 		if ((g_date_time_compare (now, then) > 0) &&
150 		    (g_date_time_compare (then, yesterday) > 0 ||
151 		     (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (yesterday) &&
152 		      g_date_time_get_year (then) == g_date_time_get_year (yesterday)))) {
153 			/* translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary */
154 			buf = e_utf8_strftime_fix_am_pm (_("%a %l:%M %p"), then);
155 			done = TRUE;
156 		}
157 		g_date_time_unref (yesterday);
158 	}
159 	if (!done) {
160 		if (g_date_time_get_year (then) == g_date_time_get_year (now)) {
161 			/* translation hint: date format for dates older than a week but from this year, reorder format codes as necessary */
162 			buf = e_utf8_strftime_fix_am_pm (_("%b %d %l:%M %p"), then);
163 		} else {
164 			/* translation hint: date format for dates from the last years, reorder format codes as necessary */
165 			buf = e_utf8_strftime_fix_am_pm (_("%b %d %Y"), then);
166 		}
167 	}
168 
169 	g_date_time_unref (then);
170 	g_date_time_unref (now);
171 
172 	temp = buf;
173 	while ((temp = strstr (temp, "  "))) {
174 		memmove (temp, temp + 1, strlen (temp));
175 	}
176 	temp = g_strstrip (buf);
177 	return temp;
178 }
179 
180 gchar *
date_format(gint64 date,const gchar * date_format)181 date_format (gint64 date, const gchar *date_format)
182 {
183 	gchar		*result;
184 	GDateTime 	*date_tm;
185 
186 	if (date == 0) {
187 		return g_strdup ("");
188 	}
189 
190 	if (date_format) {
191 		date_tm = g_date_time_new_from_unix_local (date);
192 		result = e_utf8_strftime_fix_am_pm (date_format, date_tm);
193 		g_date_time_unref (date_tm);
194 	} else {
195 		result = date_format_nice (date);
196 	}
197 
198 	return result;
199 }
200 
201 /* date parsing methods */
202 
203 gint64
date_parse_ISO8601(const gchar * date)204 date_parse_ISO8601 (const gchar *date)
205 {
206 	GTimeVal 	timeval;
207 	GDateTime 	*datetime = NULL;
208 	gboolean 	result;
209 	guint64 	year, month, day;
210 	gint64 		t = 0;
211 	gchar		*pos, *next, *ascii_date = NULL;
212 
213 	g_assert (date != NULL);
214 
215 	/* we expect at least something like "2003-08-07T15:28:19" and
216 	   don't require the second fractions and the timezone info
217 
218 	   the most specific format:   YYYY-MM-DDThh:mm:ss.sTZD
219 	 */
220 
221 	/* full specified variant */
222 	result = g_time_val_from_iso8601 (date, &timeval);
223 	if (result)
224 		return timeval.tv_sec;
225 
226 
227 	/* only date */
228 	ascii_date = g_str_to_ascii (date, "C");
229 	ascii_date = g_strstrip (ascii_date);
230 
231 	/* Parsing year */
232 	year = g_ascii_strtoull (ascii_date, &next, 10);
233 	if ((*next != '-') || (next == ascii_date))
234 		goto parsing_failed;
235 	pos = next + 1;
236 
237 	/* Parsing month */
238 	month = g_ascii_strtoull (pos, &next, 10);
239 	if ((*next != '-') || (next == pos))
240 		goto parsing_failed;
241 	pos = next + 1;
242 
243 	/* Parsing day */
244 	day = g_ascii_strtoull (pos, &next, 10);
245 	if ((*next != '\0') || (next == pos))
246 		goto parsing_failed;
247 
248 	/* there were others combinations too... */
249 
250 	datetime = g_date_time_new_utc (year, month, day, 0,0,0);
251 	if (datetime) {
252 		t = g_date_time_to_unix (datetime);
253 		g_date_time_unref (datetime);
254 	}
255 
256 parsing_failed:
257 	if (!t)
258 		debug0 (DEBUG_PARSING, "Invalid ISO8601 date format! Ignoring <dc:date> information!");
259 	g_free (ascii_date);
260 	return t;
261 }
262 
263 /* in theory, we'd need only the RFC822 timezones here
264    in practice, feeds also use other timezones...        */
265 static struct {
266 	const char *name;
267 	const char *offset;
268 } tz_offsets [] = {
269 	{ "IDLW","-1200" },
270 	{ "HAST","-1000" },
271 	{ "AKST","-0900" },
272 	{ "AKDT","-0800" },
273 	{ "WESZ","+0100" },
274 	{ "WEST","+0100" },
275 	{ "WEDT","+0100" },
276 	{ "MEST","+0200" },
277 	{ "MESZ","+0200" },
278 	{ "CEST","+0200" },
279 	{ "CEDT","+0200" },
280 	{ "EEST","+0300" },
281 	{ "EEDT","+0300" },
282 	{ "IRST","+0430" },
283 	{ "CNST","+0800" },
284 	{ "ACST","+0930" },
285 	{ "ACDT","+1030" },
286 	{ "AEST","+1000" },
287 	{ "AEDT","+1100" },
288 	{ "IDLE","+1200" },
289 	{ "NZST","+1200" },
290 	{ "NZDT","+1300" },
291 	{ "GMT", "+00" },
292 	{ "EST", "-0500" },
293 	{ "EDT", "-0400" },
294 	{ "CST", "-0600" },
295 	{ "CDT", "-0500" },
296 	{ "MST", "-0700" },
297 	{ "MDT", "-0600" },
298 	{ "PST", "-0800" },
299 	{ "PDT", "-0700" },
300 	{ "HDT", "-0900" },
301 	{ "YST", "-0900" },
302 	{ "YDT", "-0800" },
303 	{ "AST", "-0400" },
304 	{ "ADT", "-0300" },
305 	{ "VST", "-0430" },
306 	{ "NST", "-0330" },
307 	{ "NDT", "-0230" },
308 	{ "WET", "+00" },
309 	{ "WEZ", "+00" },
310 	{ "IST", "+0100" },
311 	{ "CET", "+0100" },
312 	{ "MEZ", "+0100" },
313 	{ "EET", "+0200" },
314 	{ "MSK", "+0300" },
315 	{ "MSD", "+0400" },
316 	{ "IRT", "+0330" },
317 	{ "IST", "+0530" },
318 	{ "ICT", "+0700" },
319 	{ "JST", "+0900" },
320 	{ "NFT", "+1130" },
321 	{ "UT", "+00" },
322 	{ "PT", "-0800" },
323 	{ "BT", "+0300" },
324 	{ "Z", "+00" },
325 	{ "A", "-0100" },
326 	{ "M", "-1200" },
327 	{ "N", "+0100" },
328 	{ "Y", "+1200" }
329 };
330 
331 /** date_parse_rfc822_tz:
332  * @token: String representing the timezone.
333  *
334  * Returns: (transfer full): a GTimeZone to be freed by g_time_zone_unref
335  */
336 static GTimeZone *
date_parse_rfc822_tz(char * token)337 date_parse_rfc822_tz (char *token)
338 {
339 	const char *inptr = token;
340 	int num_timezones = sizeof (tz_offsets) / sizeof ((tz_offsets)[0]);
341 
342 	if (*inptr == '+' || *inptr == '-') {
343 		return g_time_zone_new (inptr);
344 	} else {
345 		int t;
346 
347 		if (*inptr == '(')
348 			inptr++;
349 
350 		for (t = 0; t < num_timezones; t++)
351 			if (!strncmp (inptr, tz_offsets[t].name, strlen (tz_offsets[t].name)))
352 				return g_time_zone_new (tz_offsets[t].offset);
353 	}
354 
355 	return g_time_zone_new_utc ();
356 }
357 
358 static const gchar * rfc822_months[] = { "Jan", "Feb", "Mar", "Apr", "May",
359 	     "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
360 
361 GDateMonth
date_parse_month(const gchar * str)362 date_parse_month (const gchar *str)
363 {
364 	int i;
365 	for (i = 0;i < 12;i++) {
366 		if (!g_ascii_strncasecmp (str, rfc822_months[i], 3))
367 			return i + 1;
368 	}
369 	return 0;
370 }
371 
372 gint64
date_parse_RFC822(const gchar * date)373 date_parse_RFC822 (const gchar *date)
374 {
375 	guint64 	day, month, year, hour, minute, second = 0;
376 	GTimeZone	*tz = NULL;
377 	GDateTime 	*datetime = NULL;
378 	gint64		t = 0;
379 	gchar		*pos, *next, *ascii_date = NULL;
380 
381 	/* we expect at least something like "03 Dec 12 01:38:34"
382 	   and don't require a day of week or the timezone
383 
384 	   the most specific format we expect:  "Fri, 03 Dec 12 01:38:34 CET"
385 	 */
386 
387 	/* skip day of week */
388 	pos = g_utf8_strchr(date, -1, ',');
389 	if (pos)
390 		date = ++pos;
391 
392 	ascii_date = g_str_to_ascii (date, "C");
393 
394 	/* Parsing day */
395 	day = g_ascii_strtoull (ascii_date, &next, 10);
396 	if ((*next == '\0') || (next == ascii_date))
397 		goto parsing_failed;
398 	pos = next;
399 
400 	/* Parsing month */
401 	while (pos && *pos != '\0' && g_ascii_isspace (*pos))       /* skip whitespaces before month */
402 		pos++;
403 	if (strlen (pos) < 3)
404 		goto parsing_failed;
405 	month = date_parse_month (pos);
406 	pos += 3;
407 
408 	/* Parsing year */
409 	year = g_ascii_strtoull (pos, &next, 10);
410 	if ((*next == '\0') || (next == pos))
411 		goto parsing_failed;
412 	if (year < 100) {
413 		/* If year is 2 digits, years after 68 are in 20th century (strptime convention) */
414 		if (year > 68)
415 			year += 1900;
416 		else
417 			year += 2000;
418 	}
419 	pos = next;
420 
421 	/* Parsing hour */
422 	hour = g_ascii_strtoull (pos, &next, 10);
423 	if ((next == pos) || (*next != ':'))
424 		goto parsing_failed;
425 	pos = next + 1;
426 
427 	/* Parsing minute */
428 	minute = g_ascii_strtoull (pos, &next, 10);
429 	if (next == pos)
430 		goto parsing_failed;
431 
432 	/* Optional second */
433 	if (*next == ':') {
434 		pos = next + 1;
435 		second = g_ascii_strtoull (pos, &next, 10);
436 		if (next == pos)
437 			goto parsing_failed;
438 	}
439 	pos = next;
440 
441 	/* Optional Timezone */
442 	while (pos && *pos != '\0' && g_ascii_isspace (*pos))       /* skip whitespaces before timezone */
443 		pos++;
444 	if (*pos != '\0')
445 		tz = date_parse_rfc822_tz (pos);
446 
447 	if (!tz)
448 		datetime = g_date_time_new_utc (year, month, day, hour, minute, second);
449 	else {
450 		datetime = g_date_time_new (tz, year, month, day, hour, minute, second);
451 		g_time_zone_unref (tz);
452 	}
453 
454 	if (datetime) {
455 		t = g_date_time_to_unix (datetime);
456 		g_date_time_unref (datetime);
457 	}
458 
459 parsing_failed:
460 	if (!t)
461 		debug0 (DEBUG_PARSING, "Invalid RFC822 date !");
462 	g_free (ascii_date);
463 	return t;
464 }
465