Initial revision
[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   second (00..61)
50    %T   time, 24-hour (hh:mm:ss)
51    %X   locale's time representation (%H:%M:%S)
52    %Z   time zone (EDT), or nothing if no time zone is determinable
53
54    Date fields:
55    %a   locale's abbreviated weekday name (Sun..Sat)
56    %A   locale's full weekday name, variable length (Sunday..Saturday)
57    %b   locale's abbreviated month name (Jan..Dec)
58    %B   locale's full month name, variable length (January..December)
59    %c   locale's date and time (Sat Nov 04 12:02:33 EST 1989)
60    %C   century (00..99)
61    %d   day of month (01..31)
62    %e   day of month ( 1..31)
63    %D   date (mm/dd/yy)
64    %h   same as %b
65    %j   day of year (001..366)
66    %m   month (01..12)
67    %U   week number of year with Sunday as first day of week (00..53)
68    %w   day of week (0..6)
69    %W   week number of year with Monday as first day of week (00..53)
70    %x   locale's date representation (mm/dd/yy)
71    %y   last two digits of year (00..99)
72    %Y   year (1970...)
73
74    David MacKenzie <djm@gnu.ai.mit.edu> */
75
76 #include <sys/types.h>
77 #if defined(TM_IN_SYS_TIME) || (!defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME))
78 #include <sys/time.h>
79 #else
80 #include <time.h>
81 #endif
82
83 #if defined(HAVE_TZNAME)
84 extern char *tzname[2];
85 #endif
86
87 #if !__STDC__
88 #define const
89 #endif
90
91 /* Types of padding for numbers in date and time. */
92 enum padding
93 {
94   none, blank, zero
95 };
96
97 static char *days[] =
98 {
99   "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
100 };
101
102 static char *months[] =
103 {
104   "January", "February", "March", "April", "May", "June",
105   "July", "August", "September", "October", "November", "December"
106 };
107
108 /* Add character C to STRING and increment LENGTH,
109    unless LENGTH would exceed MAX. */
110
111 #define add_char(c) (length + 1 <= max) && (string[length++] = (c))
112
113 /* Add a 2 digit number to STRING, padding if specified.
114    Return the number of characters added, up to MAX. */
115
116 static int
117 add_num2 (string, num, max, pad)
118      char *string;
119      int num;
120      int max;
121      enum padding pad;
122 {
123   int top = num / 10;
124   int length = 0;
125
126   if (top == 0 && pad == blank)
127     add_char (' ');
128   else if (top != 0 || pad == zero)
129     add_char (top + '0');
130   add_char (num % 10 + '0');
131   return length;
132 }
133
134 /* Add a 3 digit number to STRING, padding if specified.
135    Return the number of characters added, up to MAX. */
136
137 static int
138 add_num3 (string, num, max, pad)
139      char *string;
140      int num;
141      int max;
142      enum padding pad;
143 {
144   int top = num / 100;
145   int mid = (num - top * 100) / 10;
146   int length = 0;
147
148   if (top == 0 && pad == blank)
149     add_char (' ');
150   else if (top != 0 || pad == zero)
151     add_char (top + '0');
152   if (mid == 0 && top == 0 && pad == blank)
153     add_char (' ');
154   else if (mid != 0 || top != 0 || pad == zero)
155     add_char (mid + '0');
156   add_char (num % 10 + '0');
157   return length;
158 }
159
160 /* Like strncpy except return the number of characters copied. */
161
162 static int
163 add_str (to, from, max)
164      char *to;
165      char *from;
166      int max;
167 {
168   int i;
169
170   for (i = 0; from[i] && i <= max; ++i)
171     to[i] = from[i];
172   return i;
173 }
174
175 /* Return the week in the year of the time in TM, with the weeks
176    starting on Sundays. */
177
178 static int
179 sun_week (tm)
180      struct tm *tm;
181 {
182   int dl;
183
184   /* Set `dl' to the day in the year of the last day of the week previous
185      to the one containing the day specified in TM.  If the day specified
186      in TM is in the first week of the year, `dl' will be negative or 0.
187      Otherwise, calculate the number of complete weeks before our week
188      (dl / 7) and add any partial week at the start of the year (dl % 7). */
189   dl = tm->tm_yday - tm->tm_wday;
190   return dl <= 0 ? 0 : dl / 7 + (dl % 7 != 0);
191 }
192
193 /* Return the week in the year of the time in TM, with the weeks
194    starting on Mondays. */
195
196 static int
197 mon_week (tm)
198      struct tm *tm;
199 {
200   int dl, wday;
201
202   if (tm->tm_wday == 0)
203     wday = 6;
204   else
205     wday = tm->tm_wday - 1;
206   dl = tm->tm_yday - wday;
207   return dl <= 0 ? 0 : dl / 7 + (dl % 7 != 0);
208 }
209
210 #if !defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME)
211 char *
212 zone_name (tp)
213      struct tm *tp;
214 {
215   char *timezone ();
216   struct timeval tv;
217   struct timezone tz;
218
219   gettimeofday (&tv, &tz);
220   return timezone (tz.tz_minuteswest, tp->tm_isdst);
221 }
222 #endif
223
224 /* Format the time given in TM according to FORMAT, and put the
225    results in STRING.
226    Return the number of characters (not including terminating null)
227    that were put into STRING, or 0 if the length would have
228    exceeded MAX. */
229
230 size_t
231 strftime (string, max, format, tm)
232      char *string;
233      size_t max;
234      const char *format;
235      const struct tm *tm;
236 {
237   enum padding pad;             /* Type of padding to apply. */
238   size_t length = 0;            /* Characters put in STRING so far. */
239
240   for (; *format && length < max; ++format)
241     {
242       if (*format != '%')
243         add_char (*format);
244       else
245         {
246           ++format;
247           /* Modifiers: */
248           if (*format == '-')
249             {
250               pad = none;
251               ++format;
252             }
253           else if (*format == '_')
254             {
255               pad = blank;
256               ++format;
257             }
258           else
259             pad = zero;
260
261           switch (*format)
262             {
263               /* Literal character fields: */
264             case 0:
265             case '%':
266               add_char ('%');
267               break;
268             case 'n':
269               add_char ('\n');
270               break;
271             case 't':
272               add_char ('\t');
273               break;
274             default:
275               add_char (*format);
276               break;
277
278               /* Time fields: */
279             case 'H':
280             case 'k':
281               length +=
282                 add_num2 (&string[length], tm->tm_hour, max - length,
283                           *format == 'H' ? pad : blank);
284               break;
285             case 'I':
286             case 'l':
287               {
288                 int hour12;
289
290                 if (tm->tm_hour == 0)
291                   hour12 = 12;
292                 else if (tm->tm_hour > 12)
293                   hour12 = tm->tm_hour - 12;
294                 else
295                   hour12 = tm->tm_hour;
296                 length +=
297                   add_num2 (&string[length], hour12, max - length,
298                             *format == 'I' ? pad : blank);
299               }
300               break;
301             case 'M':
302               length +=
303                 add_num2 (&string[length], tm->tm_min, max - length, pad);
304               break;
305             case 'p':
306               if (tm->tm_hour < 12)
307                 add_char ('A');
308               else
309                 add_char ('P');
310               add_char ('M');
311               break;
312             case 'r':
313               length +=
314                 strftime (&string[length], max - length, "%I:%M:%S %p", tm);
315               break;
316             case 'R':
317               length +=
318                 strftime (&string[length], max - length, "%H:%M", tm);
319               break;
320             case 'S':
321               length +=
322                 add_num2 (&string[length], tm->tm_sec, max - length, pad);
323               break;
324             case 'T':
325               length +=
326                 strftime (&string[length], max - length, "%H:%M:%S", tm);
327               break;
328             case 'X':
329               length +=
330                 strftime (&string[length], max - length, "%H:%M:%S", tm);
331               break;
332             case 'Z':
333 #ifdef HAVE_TM_ZONE
334               length += add_str (&string[length], tm->tm_zone, max - length);
335 #else
336 #ifdef HAVE_TZNAME
337               if (tm->tm_isdst && tzname[1] && *tzname[1])
338                 length += add_str (&string[length], tzname[1], max - length);
339               else
340                 length += add_str (&string[length], tzname[0], max - length);
341 #else
342               length += add_str (&string[length], zone_name (tm), max - length);
343 #endif
344 #endif
345               break;
346
347               /* Date fields: */
348             case 'a':
349               add_char (days[tm->tm_wday][0]);
350               add_char (days[tm->tm_wday][1]);
351               add_char (days[tm->tm_wday][2]);
352               break;
353             case 'A':
354               length +=
355                 add_str (&string[length], days[tm->tm_wday], max - length);
356               break;
357             case 'b':
358             case 'h':
359               add_char (months[tm->tm_mon][0]);
360               add_char (months[tm->tm_mon][1]);
361               add_char (months[tm->tm_mon][2]);
362               break;
363             case 'B':
364               length +=
365                 add_str (&string[length], months[tm->tm_mon], max - length);
366               break;
367             case 'c':
368               length +=
369                 strftime (&string[length], max - length,
370                           "%a %b %d %H:%M:%S %Z %Y", tm);
371               break;
372             case 'C':
373               length +=
374                 add_num2 (&string[length], (tm->tm_year + 1900) / 100,
375                           max - length, pad);
376               break;
377             case 'd':
378               length +=
379                 add_num2 (&string[length], tm->tm_mday, max - length, pad);
380               break;
381             case 'e':
382               length +=
383                 add_num2 (&string[length], tm->tm_mday, max - length, blank);
384               break;
385             case 'D':
386               length +=
387                 strftime (&string[length], max - length, "%m/%d/%y", tm);
388               break;
389             case 'j':
390               length +=
391                 add_num3 (&string[length], tm->tm_yday + 1, max - length, pad);
392               break;
393             case 'm':
394               length +=
395                 add_num2 (&string[length], tm->tm_mon + 1, max - length, pad);
396               break;
397             case 'U':
398               length +=
399                 add_num2 (&string[length], sun_week (tm), max - length, pad);
400               break;
401             case 'w':
402               add_char (tm->tm_wday + '0');
403               break;
404             case 'W':
405               length +=
406                 add_num2 (&string[length], mon_week (tm), max - length, pad);
407               break;
408             case 'x':
409               length +=
410                 strftime (&string[length], max - length, "%m/%d/%y", tm);
411               break;
412             case 'y':
413               length +=
414                 add_num2 (&string[length], tm->tm_year % 100,
415                           max - length, pad);
416               break;
417             case 'Y':
418               add_char ((tm->tm_year + 1900) / 1000 + '0');
419               length +=
420                 add_num3 (&string[length],
421                           (1900 + tm->tm_year) % 1000, max - length, zero);
422               break;
423             }
424         }
425     }
426   add_char (0);
427   return length - 1;
428 }