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