52fcbd4f3e4586368d7850f59cb60b19a694c867
[platform/upstream/libsoup.git] / libsoup / soup-date.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-date.c: Date/time handling
4  *
5  * Copyright (C) 2005, Novell, Inc.
6  * Copyright (C) 2007, Red Hat, Inc.
7  */
8
9 #ifdef HAVE_CONFIG_H
10 #include <config.h>
11 #endif
12
13 #include <stdlib.h>
14 #include <string.h>
15 #include <glib.h>
16
17 #include "soup-date.h"
18
19 /**
20  * SoupDate:
21  * @year: the year, 1 to 9999
22  * @month: the month, 1 to 12
23  * @day: day of the month, 1 to 31
24  * @hour: hour of the day, 0 to 23
25  * @minute: minute, 0 to 59
26  * @second: second, 0 to 59 (or up to 61 in the case of leap seconds)
27  * @utc: %TRUE if the date is in UTC
28  * @offset: offset from UTC
29
30  * A date and time. The date is assumed to be in the (proleptic)
31  * Gregorian calendar. The time is in UTC if @utc is %TRUE. Otherwise,
32  * the time is a local time, and @offset gives the offset from UTC in
33  * minutes (such that adding @offset to the time would give the
34  * correct UTC time). If @utc is %FALSE and @offset is 0, then the
35  * %SoupDate represents a "floating" time with no associated timezone
36  * information.
37  **/
38
39 /* Do not internationalize */
40 static const char *months[] = {
41         "Jan", "Feb", "Mar", "Apr", "May", "Jun",
42         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
43 };
44
45 /* Do not internationalize */
46 static const char *days[] = {
47         "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
48 };
49
50 static const int days_before[] = {
51         0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
52 };
53
54 GType
55 soup_date_get_type (void)
56 {
57         static volatile gsize type_volatile = 0;
58
59         if (g_once_init_enter (&type_volatile)) {
60                 GType type = g_boxed_type_register_static (
61                         g_intern_static_string ("SoupDate"),
62                         (GBoxedCopyFunc) soup_date_copy,
63                         (GBoxedFreeFunc) soup_date_free);
64                 g_once_init_leave (&type_volatile, type);
65         }
66         return type_volatile;
67 }
68
69 /**
70  * soup_date_new:
71  * @year: the year (1-9999)
72  * @month: the month (1-12)
73  * @day: the day of the month (1-31, as appropriate for @month)
74  * @hour: the hour (0-23)
75  * @minute: the minute (0-59)
76  * @second: the second (0-59)
77  *
78  * Creates a #SoupDate representing the indicated time, UTC.
79  *
80  * Return value: a new #SoupDate
81  **/
82 SoupDate *
83 soup_date_new (int year, int month, int day, 
84                int hour, int minute, int second)
85 {
86         SoupDate *date = g_slice_new (SoupDate);
87
88         date->year   = year;
89         date->month  = month;
90         date->day    = day;
91         date->hour   = hour;
92         date->minute = minute;
93         date->second = second;
94         date->utc    = TRUE;
95         date->offset = 0;
96
97         return date;
98 }
99
100 /**
101  * soup_date_new_from_now:
102  * @offset_seconds: offset from current time
103  *
104  * Creates a #SoupDate representing a time @offset_seconds after the
105  * current time (or before it, if @offset_seconds is negative). If
106  * offset_seconds is 0, returns the current time.
107  *
108  * Return value: a new #SoupDate
109  **/
110 SoupDate *
111 soup_date_new_from_now (int offset_seconds)
112 {
113         return soup_date_new_from_time_t (time (NULL) + offset_seconds);
114 }
115
116 static gboolean
117 parse_iso8601_date (SoupDate *date, const char *date_string)
118 {
119         gulong val;
120
121         if (strlen (date_string) < 15)
122                 return FALSE;
123         if (date_string[4] == '-' &&
124             date_string[7] == '-' &&
125             date_string[10] == 'T') {
126                 /* YYYY-MM-DD */
127                 date->year  = atoi (date_string);
128                 date->month = atoi (date_string + 5);
129                 date->day   = atoi (date_string + 8);
130                 date_string += 11;
131         } else if (date_string[8] == 'T') {
132                 /* YYYYMMDD */
133                 val = atoi (date_string);
134                 date->year = val / 10000;
135                 date->month = (val % 10000) / 100;
136                 date->day = val % 100;
137                 date_string += 9;
138         } else
139                 return FALSE;
140
141         if (strlen (date_string) >= 8 &&
142             date_string[2] == ':' && date_string[5] == ':') {
143                 /* HH:MM:SS */
144                 date->hour   = atoi (date_string);
145                 date->minute = atoi (date_string + 3);
146                 date->second = atoi (date_string + 6);
147                 date_string += 8;
148         } else if (strlen (date_string) >= 6) {
149                 /* HHMMSS */
150                 val = strtoul (date_string, (char **)&date_string, 10);
151                 date->hour   = val / 10000;
152                 date->minute = (val % 10000) / 100;
153                 date->second = val % 100;
154         } else
155                 return FALSE;
156
157         if (*date_string == '.')
158                 strtoul (date_string + 1, (char **)&date_string, 10);
159
160         if (*date_string == 'Z') {
161                 date_string++;
162                 date->utc = TRUE;
163                 date->offset = 0;
164         } else if (*date_string == '+' || *date_string == '-') {
165                 int sign = (*date_string == '+') ? -1 : 1;
166                 val = strtoul (date_string + 1, (char **)&date_string, 10);
167                 if (*date_string == ':')
168                         val = 60 * val + strtoul (date_string + 1, (char **)&date_string, 10);
169                 else
170                         val = 60 * (val / 100) + (val % 100);
171                 date->offset = sign * val;
172                 date->utc = sign && !val;
173         }
174
175         return !*date_string;
176 }
177
178 static inline gboolean
179 parse_day (SoupDate *date, const char **date_string)
180 {
181         char *end;
182
183         date->day = strtoul (*date_string, &end, 10);
184         if (end == (char *)date_string)
185                 return FALSE;
186
187         while (*end == ' ' || *end == '-')
188                 end++;
189         *date_string = end;
190         return TRUE;
191 }
192
193 static inline gboolean
194 parse_month (SoupDate *date, const char **date_string)
195 {
196         int i;
197
198         for (i = 0; i < G_N_ELEMENTS (months); i++) {
199                 if (!strncmp (*date_string, months[i], 3)) {
200                         date->month = i + 1;
201                         *date_string += 3;
202                         while (**date_string == ' ' || **date_string == '-')
203                                 (*date_string)++;
204                         return TRUE;
205                 }
206         }
207         return FALSE;
208 }
209
210 static inline gboolean
211 parse_year (SoupDate *date, const char **date_string)
212 {
213         char *end;
214
215         date->year = strtoul (*date_string, &end, 10);
216         if (end == (char *)date_string)
217                 return FALSE;
218
219         if (end == (char *)*date_string + 2) {
220                 if (date->year < 70)
221                         date->year += 2000;
222                 else
223                         date->year += 1900;
224         } else if (end == (char *)*date_string + 3)
225                 date->year += 1900;
226
227         while (*end == ' ' || *end == '-')
228                 end++;
229         *date_string = end;
230         return TRUE;
231 }
232
233 static inline gboolean
234 parse_time (SoupDate *date, const char **date_string)
235 {
236         char *p;
237
238         date->hour = strtoul (*date_string, &p, 10);
239         if (*p++ != ':')
240                 return FALSE;
241         date->minute = strtoul (p, &p, 10);
242         if (*p++ != ':')
243                 return FALSE;
244         date->second = strtoul (p, &p, 10);
245
246         while (*p == ' ')
247                 p++;
248         *date_string = p;
249         return TRUE;
250 }
251
252 static inline gboolean
253 parse_timezone (SoupDate *date, const char **date_string)
254 {
255         if (**date_string == '+' || **date_string == '-') {
256                 gulong val;
257                 int sign = (**date_string == '+') ? -1 : 1;
258                 val = strtoul (*date_string + 1, (char **)date_string, 10);
259                 if (**date_string != ':')
260                         return FALSE;
261                 val = 60 * val + strtoul (*date_string + 1, (char **)date_string, 10);
262                 date->offset = sign * val;
263                 date->utc = sign && !val;
264         } else if (**date_string == 'Z') {
265                 date->offset = 0;
266                 date->utc = TRUE;
267                 (*date_string)++;
268         } else if (!strcmp (*date_string, "GMT") ||
269                    !strcmp (*date_string, "UTC")) {
270                 date->offset = 0;
271                 date->utc = TRUE;
272                 (*date_string) += 3;
273         } else if (strchr ("ECMP", **date_string) &&
274                    ((*date_string)[1] == 'D' || (*date_string)[1] == 'S') &&
275                    (*date_string)[2] == 'T') {
276                 date->offset = -60 * (5 * strcspn ("ECMP", *date_string));
277                 if ((*date_string)[1] == 'D')
278                         date->offset += 60;
279                 date->utc = FALSE;
280         } else if (!**date_string) {
281                 date->utc = FALSE;
282                 date->offset = 0;
283         } else
284                 return FALSE;
285         return TRUE;
286 }
287
288 static gboolean
289 parse_textual_date (SoupDate *date, const char *date_string)
290 {
291         /* If it starts with a word, it must be a weekday, which we skip */
292         while (g_ascii_isalpha (*date_string))
293                 date_string++;
294         if (*date_string == ',')
295                 date_string++;
296         while (g_ascii_isspace (*date_string))
297                 date_string++;
298
299         /* If there's now another word, this must be an asctime-date */
300         if (g_ascii_isalpha (*date_string)) {
301                 /* (Sun) Nov  6 08:49:37 1994 */
302                 if (!parse_month (date, &date_string) ||
303                     !parse_day (date, &date_string) ||
304                     !parse_time (date, &date_string) ||
305                     !parse_year (date, &date_string))
306                         return FALSE;
307
308                 /* There shouldn't be a timezone, but check anyway */
309                 parse_timezone (date, &date_string);
310         } else {
311                 /* Non-asctime date, so some variation of
312                  * (Sun,) 06 Nov 1994 08:49:37 GMT
313                  */
314                 if (!parse_day (date, &date_string) ||
315                     !parse_month (date, &date_string) ||
316                     !parse_year (date, &date_string) ||
317                     !parse_time (date, &date_string))
318                         return FALSE;
319
320                 /* This time there *should* be a timezone, but we
321                  * survive if there isn't.
322                  */
323                 parse_timezone (date, &date_string);
324         }
325         return TRUE;
326 }
327
328 static int
329 days_in_month (int month, int year)
330 {
331         return days_before[month + 1] - days_before[month] +
332                 (((year % 4 == 0) && month == 2) ? 1 : 0);
333 }
334
335 /**
336  * SoupDateFormat:
337  * @SOUP_DATE_HTTP: RFC 1123 format, used by the HTTP "Date" header. Eg
338  * "Sun, 06 Nov 1994 08:49:37 GMT"
339  * @SOUP_DATE_COOKIE: The format for the "Expires" timestamp in the
340  * Netscape cookie specification. Eg, "Sun, 06-Nov-1994 08:49:37 GMT".
341  * @SOUP_DATE_RFC2822: RFC 2822 format, eg "Sun, 6 Nov 1994 09:49:37 -0100"
342  * @SOUP_DATE_ISO8601_COMPACT: ISO 8601 date/time with no optional
343  * punctuation. Eg, "19941106T094937-0100".
344  * @SOUP_DATE_ISO8601_FULL: ISO 8601 date/time with all optional
345  * punctuation. Eg, "1994-11-06T09:49:37-01:00".
346  * @SOUP_DATE_ISO8601_XMLRPC: ISO 8601 date/time as used by XML-RPC.
347  * Eg, "19941106T09:49:37".
348  * @SOUP_DATE_ISO8601: An alias for @SOUP_DATE_ISO8601_FULL.
349  *
350  * Date formats that soup_date_to_string() can use.
351  *
352  * @SOUP_DATE_HTTP and @SOUP_DATE_COOKIE always coerce the time to
353  * UTC. @SOUP_DATE_ISO8601_XMLRPC uses the time as given, ignoring the
354  * offset completely. @SOUP_DATE_RFC2822 and the other ISO 8601
355  * variants use the local time, appending the offset information if
356  * available.
357  *
358  * This enum may be extended with more values in future releases.
359  **/
360
361 /**
362  * soup_date_new_from_string:
363  * @date_string: the date in some plausible format
364  *
365  * Parses @date_string and tries to extract a date from it. This
366  * recognizes all of the "HTTP-date" formats from RFC 2616, all ISO
367  * 8601 formats containing both a time and a date, RFC 2822 dates,
368  * and reasonable approximations thereof. (Eg, it is lenient about
369  * whitespace, leading "0"s, etc.)
370  *
371  * Return value: a new #SoupDate
372  **/
373 SoupDate *
374 soup_date_new_from_string (const char *date_string)
375 {
376         SoupDate *date = g_slice_new (SoupDate);
377         gboolean success;
378
379         while (g_ascii_isspace (*date_string))
380                 date_string++;
381
382         /* If it starts with a digit, it's either an ISO 8601 date, or
383          * an RFC2822 date without the optional weekday; in the later
384          * case, there will be a month name later on, so look for one
385          * of the month-start letters.
386          */
387         if (g_ascii_isdigit (*date_string) &&
388             !strpbrk (date_string, "JFMASOND"))
389                 success = parse_iso8601_date (date, date_string);
390         else
391                 success = parse_textual_date (date, date_string);
392
393         if (!success) {
394                 g_slice_free (SoupDate, date);
395                 return NULL;
396         }
397
398         if (date->year < 1 || date->year > 9999 ||
399             date->month < 1 || date->month > 12 ||
400             date->day < 1 ||
401             date->day > days_in_month (date->month, date->year) ||
402             date->hour < 0 || date->hour > 23 ||
403             date->minute < 0 || date->minute > 59 ||
404             date->second < 0 || date->second > 59) {
405                 g_slice_free (SoupDate, date);
406                 return NULL;
407         } else
408                 return date;
409 }
410
411 /**
412  * soup_date_new_from_time_t:
413  * @when: a #time_t
414  *
415  * Creates a #SoupDate corresponding to @when
416  *
417  * Return value: a new #SoupDate
418  **/
419 SoupDate *
420 soup_date_new_from_time_t (time_t when)
421 {
422         struct tm tm;
423
424 #ifdef HAVE_GMTIME_R
425         gmtime_r (&when, &tm);
426 #else
427         tm = *gmtime (&when);
428 #endif
429
430         return soup_date_new (tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
431                               tm.tm_hour, tm.tm_min, tm.tm_sec);
432 }
433
434 static const char *
435 soup_date_weekday (SoupDate *date)
436 {
437         int day;
438
439         /* Proleptic Gregorian 0001-01-01 was a Monday, which
440          * corresponds to 1 in the days[] array. So we take the
441          * number of days since 0000-12-31, modulo 7.
442          */
443         day = (date->year - 1) * 365 + ((date->year - 1) / 4);
444         day += days_before[date->month] + date->day;
445         if (date->year % 4 == 0 && date->month > 2)
446                 day++;
447
448         return days[day % 7];
449 }
450
451 /**
452  * soup_date_to_string:
453  * @date: a #SoupDate
454  * @format: the format to generate the date in
455  *
456  * Converts @date to a string in the format described by @format.
457  *
458  * Return value: @date as a string
459  **/
460 char *
461 soup_date_to_string (SoupDate *date, SoupDateFormat format)
462 {
463         /* FIXME: offset, 8601 zones, etc */
464
465         switch (format) {
466         case SOUP_DATE_HTTP:
467                 /* "Sun, 06 Nov 1994 08:49:37 GMT" */
468                 return g_strdup_printf ("%s, %02d %s %04d %02d:%02d:%02d GMT",
469                                         soup_date_weekday (date), date->day,
470                                         months[date->month - 1],
471                                         date->year, date->hour, date->minute,
472                                         date->second);
473
474         case SOUP_DATE_COOKIE:
475                 /* "Sun, 06-Nov-1994 08:49:37 GMT" */
476                 return g_strdup_printf ("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
477                                         soup_date_weekday (date), date->day,
478                                         months[date->month - 1],
479                                         date->year, date->hour, date->minute,
480                                         date->second);
481
482         case SOUP_DATE_ISO8601_COMPACT:
483                 return g_strdup_printf ("%04d%02d%02dT%02d%02d%02d",
484                                         date->year, date->month, date->day,
485                                         date->hour, date->minute, date->second);
486         case SOUP_DATE_ISO8601_FULL:
487                 return g_strdup_printf ("%04d-%02d-%02dT%02d:%02d:%02d",
488                                         date->year, date->month, date->day,
489                                         date->hour, date->minute, date->second);
490         case SOUP_DATE_ISO8601_XMLRPC:
491                 return g_strdup_printf ("%04d%02d%02dT%02d:%02d:%02d",
492                                         date->year, date->month, date->day,
493                                         date->hour, date->minute, date->second);
494
495         default:
496                 return NULL;
497         }
498 }
499
500 /**
501  * soup_date_to_time_t:
502  * @date: a #SoupDate
503  *
504  * Converts @date to a %time_t
505  *
506  * Return value: @date as a %time_t
507  **/
508 time_t
509 soup_date_to_time_t (SoupDate *date)
510 {
511 #ifndef HAVE_TIMEGM
512         time_t tt;
513         static const int days_before[] = {
514                 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
515         };
516 #endif
517         struct tm tm;
518
519         /* FIXME: offset, etc */
520
521         tm.tm_year = date->year - 1900;
522         tm.tm_mon  = date->month - 1;
523         tm.tm_mday = date->day;
524         tm.tm_hour = date->hour;
525         tm.tm_min  = date->minute;
526         tm.tm_sec  = date->second;
527
528 #if HAVE_TIMEGM
529         return timegm (&tm);
530 #else
531         /* We check the month because (a) if we don't, the
532          * days_before[] part below may access random memory, and (b)
533          * soup_date_parse() doesn't check the return value of
534          * parse_month(). The caller is responsible for ensuring the
535          * sanity of everything else.
536          */
537         if (tm.tm_mon < 0 || tm.tm_mon > 11)
538                 return (time_t)-1;
539
540         tt = (tm.tm_year - 70) * 365;
541         tt += (tm.tm_year - 68) / 4;
542         tt += days_before[tm.tm_mon] + tm.tm_mday - 1;
543         if (tm.tm_year % 4 == 0 && tm.tm_mon < 2)
544                 tt--;
545         tt = ((((tt * 24) + tm.tm_hour) * 60) + tm.tm_min) * 60 + tm.tm_sec;
546         
547         return tt;
548 #endif
549 }
550
551 /**
552  * soup_date_copy:
553  * @date: a #SoupDate
554  *
555  * Copies @date.
556  **/
557 SoupDate *
558 soup_date_copy (SoupDate *date)
559 {
560         SoupDate *copy = g_slice_new (SoupDate);
561
562         memcpy (copy, date, sizeof (SoupDate));
563         return copy;
564 }
565
566 /**
567  * soup_date_free:
568  * @date: a #SoupDate
569  *
570  * Frees @date.
571  **/
572 void
573 soup_date_free (SoupDate *date)
574 {
575         g_slice_free (SoupDate, date);
576 }