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