.
[gnulib.git] / lib / strftime.c
1 /* strftime - custom formatting of date and/or time
2    Copyright (C) 1989, 1991, 1992 Free Software Foundation, Inc.
3
4    This program is free software; you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; either version 2, or (at your option)
7    any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program; if not, write to the Free Software
16    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
17
18 /* Note: this version of strftime lacks locale support,
19    but it is standalone.
20
21    Performs `%' substitutions similar to those in printf.  Except
22    where noted, substituted fields have a fixed size; numeric fields are
23    padded if necessary.  Padding is with zeros by default; for fields
24    that display a single number, padding can be changed or inhibited by
25    following the `%' with one of the modifiers described below.  Unknown
26    field specifiers are copied as normal characters.  All other
27    characters are copied to the output without change.
28
29    Supports a superset of the ANSI C field specifiers.
30
31    Literal character fields:
32    %    %
33    n    newline
34    t    tab
35
36    Numeric modifiers (a nonstandard extension):
37    -    do not pad the field
38    _    pad the field with spaces
39
40    Time fields:
41    %H   hour (00..23)
42    %I   hour (01..12)
43    %k   hour ( 0..23)
44    %l   hour ( 1..12)
45    %M   minute (00..59)
46    %p   locale's AM or PM
47    %r   time, 12-hour (hh:mm:ss [AP]M)
48    %R   time, 24-hour (hh:mm)
49    %s   time in seconds since 00:00:00, Jan 1, 1970 (a nonstandard extension)
50    %S   second (00..61)
51    %T   time, 24-hour (hh:mm:ss)
52    %X   locale's time representation (%H:%M:%S)
53    %z   RFC-822 style numeric timezone (-0500) (a nonstandard extension)
54    %Z   time zone (EDT), or nothing if no time zone is determinable
55
56    Date fields:
57    %a   locale's abbreviated weekday name (Sun..Sat)
58    %A   locale's full weekday name, variable length (Sunday..Saturday)
59    %b   locale's abbreviated month name (Jan..Dec)
60    %B   locale's full month name, variable length (January..December)
61    %c   locale's date and time (Sat Nov 04 12:02:33 EST 1989)
62    %C   century (00..99)
63    %d   day of month (01..31)
64    %e   day of month ( 1..31)
65    %D   date (mm/dd/yy)
66    %h   same as %b
67    %j   day of year (001..366)
68    %m   month (01..12)
69    %U   week number of year with Sunday as first day of week (00..53)
70    %V   FIXME
71    %w   day of week (0..6)
72    %W   week number of year with Monday as first day of week (00..53)
73    %x   locale's date representation (mm/dd/yy)
74    %y   last two digits of year (00..99)
75    %Y   year (1970...)
76
77    David MacKenzie <djm@gnu.ai.mit.edu> */
78
79 #ifdef HAVE_CONFIG_H
80 #include <config.h>
81 #endif
82
83 #include <stdio.h>
84 #include <sys/types.h>
85 #if defined(TM_IN_SYS_TIME) || (!defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME))
86 #include <sys/time.h>
87 #else
88 #include <time.h>
89 #endif
90
91 #ifndef STDC_HEADERS
92 time_t mktime ();
93 #endif
94
95 #if defined(HAVE_TZNAME)
96 extern char *tzname[2];
97 #endif
98
99 /* Types of padding for numbers in date and time. */
100 enum padding
101 {
102   none, blank, zero
103 };
104
105 static char const* const days[] =
106 {
107   "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
108 };
109
110 static char const * const months[] =
111 {
112   "January", "February", "March", "April", "May", "June",
113   "July", "August", "September", "October", "November", "December"
114 };
115
116 /* Add character C to STRING and increment LENGTH,
117    unless LENGTH would exceed MAX. */
118
119 #define add_char(c)                                                     \
120   do                                                                    \
121     {                                                                   \
122       if (length + 1 <= max)                                            \
123         string[length++] = (c);                                         \
124     }                                                                   \
125   while (0)
126
127 /* Add a 2 digit number to STRING, padding if specified.
128    Return the number of characters added, up to MAX. */
129
130 static int
131 add_num2 (string, num, max, pad)
132      char *string;
133      int num;
134      int max;
135      enum padding pad;
136 {
137   int top = num / 10;
138   int length = 0;
139
140   if (top == 0 && pad == blank)
141     add_char (' ');
142   else if (top != 0 || pad == zero)
143     add_char (top + '0');
144   add_char (num % 10 + '0');
145   return length;
146 }
147
148 /* Add a 3 digit number to STRING, padding if specified.
149    Return the number of characters added, up to MAX. */
150
151 static int
152 add_num3 (string, num, max, pad)
153      char *string;
154      int num;
155      int max;
156      enum padding pad;
157 {
158   int top = num / 100;
159   int mid = (num - top * 100) / 10;
160   int length = 0;
161
162   if (top == 0 && pad == blank)
163     add_char (' ');
164   else if (top != 0 || pad == zero)
165     add_char (top + '0');
166   if (mid == 0 && top == 0 && pad == blank)
167     add_char (' ');
168   else if (mid != 0 || top != 0 || pad == zero)
169     add_char (mid + '0');
170   add_char (num % 10 + '0');
171   return length;
172 }
173
174 /* Like strncpy except return the number of characters copied. */
175
176 static int
177 add_str (to, from, max)
178      char *to;
179      const char *from;
180      int max;
181 {
182   int i;
183
184   for (i = 0; from[i] && i <= max; ++i)
185     to[i] = from[i];
186   return i;
187 }
188
189 static int
190 add_num_time_t (string, max, num)
191      char *string;
192      int max;
193      time_t num;
194 {
195   /* This buffer is large enough to hold the character representation
196      (including the trailing NUL) of any unsigned decimal quantity
197      whose binary representation fits in 128 bits.  */
198   char buf[40];
199   int length;
200
201   if (sizeof (num) > 16)
202     abort ();
203   sprintf (buf, "%lu", (unsigned long) num);
204   length = add_str (string, buf, max);
205   return length;
206 }
207
208 /* Convert MINUTES_EAST into a string suitable for use as the RFC-822
209    timezone indicator.  Write no more than MAX bytes into STRING.
210     Return the number of bytes written into STRING.  */
211
212 static int
213 add_num_tz (string, max, minutes_east)
214      char *string;
215      int max;
216      int minutes_east;
217 {
218   int length;
219
220   if (max < 1)
221     return 0;
222
223   if (minutes_east < 0)
224     {
225       *string = '-';
226       minutes_east = -minutes_east;
227     }
228   else
229     *string = '+';
230
231   length = 1 + add_num2 (&string[1], (minutes_east / 60) % 24, max - 1, zero);
232   length += add_num2 (&string[length], minutes_east % 60, max - length, zero);
233
234   return length;
235 }
236
237 /* Implement %U.  Return the week in the year of the time in TM,
238    with the weeks starting on Sundays.  */
239
240 static int
241 sun_week (tm)
242      const struct tm *tm;
243 {
244   int dl;
245
246   /* %U Week of the year (Sunday as the first day of the week) as a decimal
247      number [00-53].  All days in a new year preceding the first Sunday are
248      considered to be in week 0.  */
249
250   dl = tm->tm_yday - tm->tm_wday;
251   return dl < 0 ? 0 : dl / 7 + 1;
252 }
253
254 /* Implement %V.  Similar to mon_week (%W), but there is no 0'th week --
255    they're numbered [01-53].  And if the week containing January 1 has
256    four or more days in the new year, then it is considered week 1;
257    otherwise, it is week 53 of the previous year, and the next week is
258    week 1. (See the ISO 8601: 1988 standard.)  */
259
260 static int
261 mon_week_ISO (tm)
262      const struct tm *tm;
263 {
264   int dl, n_days_before_first_monday;
265   int week_num;
266
267   n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
268   dl = tm->tm_yday - n_days_before_first_monday;
269   week_num = dl < 0 ? 0 : dl / 7 + 1;
270   if (n_days_before_first_monday >= 4)
271     {
272       week_num = (week_num + 1) % 54;
273       if (week_num == 0)
274         week_num = 1;
275     }
276   if (week_num == 0)
277     week_num = 53;
278
279   return week_num;
280 }
281
282 /* Implement %W.  Return the week in the year of the time in TM,
283    with the weeks starting on Mondays.  */
284
285 static int
286 mon_week (tm)
287      const struct tm *tm;
288 {
289   int dl, n_days_before_first_monday;
290
291   n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
292   dl = tm->tm_yday - n_days_before_first_monday;
293   return dl < 0 ? 0 : dl / 7 + 1;
294 }
295
296 #if !defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME)
297 char *
298 zone_name (tp)
299      struct tm *tp;
300 {
301   char *timezone ();
302   struct timeval tv;
303   struct timezone tz;
304
305   gettimeofday (&tv, &tz);
306   return timezone (tz.tz_minuteswest, tp->tm_isdst);
307 }
308 #endif
309
310 /* Format the time given in TM according to FORMAT, and put the
311    results in STRING.
312    Return the number of characters (not including terminating null)
313    that were put into STRING, or 0 if the length would have
314    exceeded MAX. */
315
316 size_t
317 strftime (string, max, format, tm)
318      char *string;
319      size_t max;
320      const char *format;
321      const struct tm *tm;
322 {
323   enum padding pad;             /* Type of padding to apply. */
324   size_t length = 0;            /* Characters put in STRING so far. */
325
326   for (; *format && length < max; ++format)
327     {
328       if (*format != '%')
329         add_char (*format);
330       else
331         {
332           ++format;
333           /* Modifiers: */
334           if (*format == '-')
335             {
336               pad = none;
337               ++format;
338             }
339           else if (*format == '_')
340             {
341               pad = blank;
342               ++format;
343             }
344           else
345             pad = zero;
346
347           switch (*format)
348             {
349               /* Literal character fields: */
350             case 0:
351             case '%':
352               add_char ('%');
353               break;
354             case 'n':
355               add_char ('\n');
356               break;
357             case 't':
358               add_char ('\t');
359               break;
360             default:
361               add_char (*format);
362               break;
363
364               /* Time fields: */
365             case 'H':
366             case 'k':
367               length +=
368                 add_num2 (&string[length], tm->tm_hour, max - length,
369                           *format == 'H' ? pad : blank);
370               break;
371             case 'I':
372             case 'l':
373               {
374                 int hour12;
375
376                 if (tm->tm_hour == 0)
377                   hour12 = 12;
378                 else if (tm->tm_hour > 12)
379                   hour12 = tm->tm_hour - 12;
380                 else
381                   hour12 = tm->tm_hour;
382                 length +=
383                   add_num2 (&string[length], hour12, max - length,
384                             *format == 'I' ? pad : blank);
385               }
386               break;
387             case 'M':
388               length +=
389                 add_num2 (&string[length], tm->tm_min, max - length, pad);
390               break;
391             case 'p':
392               if (tm->tm_hour < 12)
393                 add_char ('A');
394               else
395                 add_char ('P');
396               add_char ('M');
397               break;
398             case 'r':
399               length +=
400                 strftime (&string[length], max - length, "%I:%M:%S %p", tm);
401               break;
402             case 'R':
403               length +=
404                 strftime (&string[length], max - length, "%H:%M", tm);
405               break;
406
407             case 's':
408               {
409                 struct tm writable_tm;
410                 writable_tm = *tm;
411                 length += add_num_time_t (&string[length], max - length,
412                                           mktime (&writable_tm));
413               }
414               break;
415
416             case 'S':
417               length +=
418                 add_num2 (&string[length], tm->tm_sec, max - length, pad);
419               break;
420             case 'T':
421               length +=
422                 strftime (&string[length], max - length, "%H:%M:%S", tm);
423               break;
424             case 'X':
425               length +=
426                 strftime (&string[length], max - length, "%H:%M:%S", tm);
427               break;
428             case 'z':
429               {
430                 time_t t;
431                 struct tm tml, tmg;
432                 int diff;
433
434                 tml = *tm;
435                 t = mktime (&tml);
436                 tml = *localtime (&t); /* Canonicalize the local time */
437                 tmg = *gmtime (&t);
438
439                 /* Compute the difference */
440
441                 diff = tml.tm_min - tmg.tm_min;
442                 diff += 60 * (tml.tm_hour - tmg.tm_hour);
443
444                 if (tml.tm_mon != tmg.tm_mon)
445                   {
446                     /* We assume no timezone differs from UTC by more than
447                        +- 23 hours.  This should be safe. */
448                     if (tmg.tm_mday == 1)
449                       tml.tm_mday = 0;
450                     else /* tml.tm_mday == 1 */
451                       tmg.tm_mday = 0;
452                   }
453
454                 diff += 1440 * (tml.tm_mday - tmg.tm_mday);
455
456                 length += add_num_tz (&string[length], max - length, diff);
457               }
458               break;
459             case 'Z':
460 #ifdef HAVE_TM_ZONE
461               length += add_str (&string[length], tm->tm_zone, max - length);
462 #else
463 #ifdef HAVE_TZNAME
464               if (tm->tm_isdst && tzname[1] && *tzname[1])
465                 length += add_str (&string[length], tzname[1], max - length);
466               else
467                 length += add_str (&string[length], tzname[0], max - length);
468 #else
469               length += add_str (&string[length], zone_name (tm), max - length);
470 #endif
471 #endif
472               break;
473
474               /* Date fields: */
475             case 'a':
476               add_char (days[tm->tm_wday][0]);
477               add_char (days[tm->tm_wday][1]);
478               add_char (days[tm->tm_wday][2]);
479               break;
480             case 'A':
481               length +=
482                 add_str (&string[length], days[tm->tm_wday], max - length);
483               break;
484             case 'b':
485             case 'h':
486               add_char (months[tm->tm_mon][0]);
487               add_char (months[tm->tm_mon][1]);
488               add_char (months[tm->tm_mon][2]);
489               break;
490             case 'B':
491               length +=
492                 add_str (&string[length], months[tm->tm_mon], max - length);
493               break;
494             case 'c':
495               length +=
496                 strftime (&string[length], max - length,
497                           "%a %b %d %H:%M:%S %Z %Y", tm);
498               break;
499             case 'C':
500               length +=
501                 add_num2 (&string[length], (tm->tm_year + 1900) / 100,
502                           max - length, pad);
503               break;
504             case 'd':
505               length +=
506                 add_num2 (&string[length], tm->tm_mday, max - length, pad);
507               break;
508             case 'e':
509               length +=
510                 add_num2 (&string[length], tm->tm_mday, max - length, blank);
511               break;
512             case 'D':
513               length +=
514                 strftime (&string[length], max - length, "%m/%d/%y", tm);
515               break;
516             case 'j':
517               length +=
518                 add_num3 (&string[length], tm->tm_yday + 1, max - length, pad);
519               break;
520             case 'm':
521               length +=
522                 add_num2 (&string[length], tm->tm_mon + 1, max - length, pad);
523               break;
524             case 'U':
525               length +=
526                 add_num2 (&string[length], sun_week (tm), max - length, pad);
527               break;
528             case 'V':
529               length += add_num2 (&string[length], mon_week_ISO (tm),
530                                   max - length, pad);
531               break;
532             case 'w':
533               add_char (tm->tm_wday + '0');
534               break;
535             case 'W':
536               length +=
537                 add_num2 (&string[length], mon_week (tm), max - length, pad);
538               break;
539             case 'x':
540               length +=
541                 strftime (&string[length], max - length, "%m/%d/%y", tm);
542               break;
543             case 'y':
544               length +=
545                 add_num2 (&string[length], tm->tm_year % 100,
546                           max - length, pad);
547               break;
548             case 'Y':
549               add_char ((tm->tm_year + 1900) / 1000 + '0');
550               length +=
551                 add_num3 (&string[length],
552                           (1900 + tm->tm_year) % 1000, max - length, zero);
553               break;
554             }
555         }
556     }
557   add_char (0);
558   return length - 1;
559 }