xref: /netbsd-src/lib/libc/time/strftime.c (revision 8e33eff89e26cf71871ead62f0d5063e1313c33a)
1 /*	$NetBSD: strftime.c,v 1.56 2024/06/07 13:53:23 riastradh Exp $	*/
2 
3 /* Convert a broken-down timestamp to a string.  */
4 
5 /* Copyright 1989 The Regents of the University of California.
6    All rights reserved.
7 
8    Redistribution and use in source and binary forms, with or without
9    modification, are permitted provided that the following conditions
10    are met:
11    1. Redistributions of source code must retain the above copyright
12       notice, this list of conditions and the following disclaimer.
13    2. Redistributions in binary form must reproduce the above copyright
14       notice, this list of conditions and the following disclaimer in the
15       documentation and/or other materials provided with the distribution.
16    3. Neither the name of the University nor the names of its contributors
17       may be used to endorse or promote products derived from this software
18       without specific prior written permission.
19 
20    THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
21    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23    ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
24    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26    OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29    OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30    SUCH DAMAGE.  */
31 
32 #include <sys/cdefs.h>
33 #if defined(LIBC_SCCS) && !defined(lint)
34 #if 0
35 static char	elsieid[] = "@(#)strftime.c	7.64";
36 static char	elsieid[] = "@(#)strftime.c	8.3";
37 #else
38 __RCSID("$NetBSD: strftime.c,v 1.56 2024/06/07 13:53:23 riastradh Exp $");
39 #endif
40 #endif /* LIBC_SCCS and not lint */
41 
42 #include "namespace.h"
43 
44 #include <stddef.h>
45 #include <assert.h>
46 #include <locale.h>
47 #include "setlocale_local.h"
48 
49 /*
50 ** Based on the UCB version with the copyright notice appearing above.
51 **
52 ** This is ANSIish only when "multibyte character == plain character".
53 */
54 
55 #include "private.h"
56 
57 /*
58 ** We don't use these extensions in strftime operation even when
59 ** supported by the local tzcode configuration.  A strictly
60 ** conforming C application may leave them in undefined state.
61 */
62 
63 #ifdef _LIBC
64 #undef TM_ZONE
65 #undef TM_GMTOFF
66 #endif
67 
68 #include <fcntl.h>
69 #include <locale.h>
70 #include <stdio.h>
71 
72 #ifndef DEPRECATE_TWO_DIGIT_YEARS
73 # define DEPRECATE_TWO_DIGIT_YEARS false
74 #endif
75 
76 #ifdef __weak_alias
77 __weak_alias(strftime_l, _strftime_l)
78 __weak_alias(strftime_lz, _strftime_lz)
79 __weak_alias(strftime_z, _strftime_z)
80 #endif
81 
82 #include "sys/localedef.h"
83 #define _TIME_LOCALE(loc) \
84     ((_TimeLocale *)((loc)->part_impl[LC_TIME]))
85 #define c_fmt   d_t_fmt
86 
87 enum warn { IN_NONE, IN_SOME, IN_THIS, IN_ALL };
88 
89 static char *	_add(const char *, char *, const char *);
90 static char *	_conv(int, const char *, char *, const char *, locale_t);
91 static char *	_fmt(const timezone_t, const char *, const struct tm *, char *,
92 			const char *, enum warn *, locale_t);
93 static char *	_yconv(int, int, bool, bool, char *, const char *, locale_t);
94 
95 #ifndef YEAR_2000_NAME
96 # define YEAR_2000_NAME "CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS"
97 #endif /* !defined YEAR_2000_NAME */
98 
99 #define	IN_NONE	0
100 #define	IN_SOME	1
101 #define	IN_THIS	2
102 #define	IN_ALL	3
103 
104 #define	PAD_DEFAULT	0
105 #define	PAD_LESS	1
106 #define	PAD_SPACE	2
107 #define	PAD_ZERO	3
108 
109 static const char fmt_padding[][4][5] = {
110 	/* DEFAULT,	LESS,	SPACE,	ZERO */
111 #define	PAD_FMT_MONTHDAY	0
112 #define	PAD_FMT_HMS		0
113 #define	PAD_FMT_CENTURY		0
114 #define	PAD_FMT_SHORTYEAR	0
115 #define	PAD_FMT_MONTH		0
116 #define	PAD_FMT_WEEKOFYEAR	0
117 #define	PAD_FMT_DAYOFMONTH	0
118 	{ "%02d",	"%d",	"%2d",	"%02d" },
119 #define	PAD_FMT_SDAYOFMONTH	1
120 #define	PAD_FMT_SHMS		1
121 	{ "%2d",	"%d",	"%2d",	"%02d" },
122 #define	PAD_FMT_DAYOFYEAR	2
123 	{ "%03d",	"%d",	"%3d",	"%03d" },
124 #define	PAD_FMT_YEAR		3
125 	{ "%04d",	"%d",	"%4d",	"%04d" }
126 };
127 
128 size_t
129 strftime_z(const timezone_t sp, char * __restrict s, size_t maxsize,
130     const char * __restrict format, const struct tm * __restrict t)
131 {
132 	return strftime_lz(sp, s, maxsize, format, t, _current_locale());
133 }
134 
135 #if HAVE_STRFTIME_L
136 size_t
137 strftime_l(char *restrict s, size_t maxsize, char const *restrict format,
138 	   struct tm const *restrict t,
139 	   ATTRIBUTE_MAYBE_UNUSED locale_t locale)
140 {
141   /* Just call strftime, as only the C locale is supported.  */
142   return strftime(s, maxsize, format, t);
143 }
144 #endif
145 
146 size_t
147 strftime_lz(const timezone_t sp, char *const s, const size_t maxsize,
148     const char *const format, const struct tm *const t, locale_t loc)
149 {
150 	char *	p;
151 	int saved_errno = errno;
152 	enum warn warn = IN_NONE;
153 
154 	p = _fmt(sp, format, t, s, s + maxsize, &warn, loc);
155 	if (!p) {
156 		errno = EOVERFLOW;
157 		return 0;
158 	}
159 	if (/*CONSTCOND*/DEPRECATE_TWO_DIGIT_YEARS
160 	    && warn != IN_NONE && getenv(YEAR_2000_NAME)) {
161 		(void) fprintf(stderr, "\n");
162 		(void) fprintf(stderr, "strftime format \"%s\" ", format);
163 		(void) fprintf(stderr, "yields only two digits of years in ");
164 		if (warn == IN_SOME)
165 			(void) fprintf(stderr, "some locales");
166 		else if (warn == IN_THIS)
167 			(void) fprintf(stderr, "the current locale");
168 		else	(void) fprintf(stderr, "all locales");
169 		(void) fprintf(stderr, "\n");
170 	}
171 	if (p == s + maxsize) {
172 		errno = ERANGE;
173 		return 0;
174 	}
175 	*p = '\0';
176 	errno = saved_errno;
177 	return p - s;
178 }
179 
180 static char *
181 _fmt(const timezone_t sp, const char *format, const struct tm *t, char *pt,
182      const char *ptlim, enum warn *warnp, locale_t loc)
183 {
184 	int Ealternative, Oalternative, PadIndex;
185 	_TimeLocale *tptr = _TIME_LOCALE(loc);
186 
187 	for ( ; *format; ++format) {
188 		if (*format == '%') {
189 			Ealternative = 0;
190 			Oalternative = 0;
191 			PadIndex = PAD_DEFAULT;
192 label:
193 			switch (*++format) {
194 			case '\0':
195 				--format;
196 				break;
197 			case 'A':
198 				pt = _add((t->tm_wday < 0 ||
199 					t->tm_wday >= DAYSPERWEEK) ?
200 					"?" : tptr->day[t->tm_wday],
201 					pt, ptlim);
202 				continue;
203 			case 'a':
204 				pt = _add((t->tm_wday < 0 ||
205 					t->tm_wday >= DAYSPERWEEK) ?
206 					"?" : tptr->abday[t->tm_wday],
207 					pt, ptlim);
208 				continue;
209 			case 'B':
210 				pt = _add((t->tm_mon < 0 ||
211 					t->tm_mon >= MONSPERYEAR) ?
212 					"?" :
213 					/* no alt_month in _TimeLocale */
214 					(Oalternative ? tptr->mon/*alt_month*/:
215 					tptr->mon)[t->tm_mon],
216 					pt, ptlim);
217 				continue;
218 			case 'b':
219 			case 'h':
220 				pt = _add((t->tm_mon < 0 ||
221 					t->tm_mon >= MONSPERYEAR) ?
222 					"?" : tptr->abmon[t->tm_mon],
223 					pt, ptlim);
224 				continue;
225 			case 'C':
226 				/*
227 				** %C used to do a...
228 				**	_fmt("%a %b %e %X %Y", t);
229 				** ...whereas now POSIX 1003.2 calls for
230 				** something completely different.
231 				** (ado, 1993-05-24)
232 				*/
233 				pt = _yconv(t->tm_year, TM_YEAR_BASE,
234 					    true, false, pt, ptlim, loc);
235 				continue;
236 			case 'c':
237 				{
238 				enum warn warn2 = IN_SOME;
239 
240 				pt = _fmt(sp, tptr->c_fmt, t, pt,
241 				    ptlim, &warn2, loc);
242 				if (warn2 == IN_ALL)
243 					warn2 = IN_THIS;
244 				if (warn2 > *warnp)
245 					*warnp = warn2;
246 				}
247 				continue;
248 			case 'D':
249 				pt = _fmt(sp, "%m/%d/%y", t, pt, ptlim, warnp,
250 				    loc);
251 				continue;
252 			case 'd':
253 				pt = _conv(t->tm_mday,
254 				    fmt_padding[PAD_FMT_DAYOFMONTH][PadIndex],
255 				    pt, ptlim, loc);
256 				continue;
257 			case 'E':
258 				if (Ealternative || Oalternative)
259 					break;
260 				Ealternative++;
261 				goto label;
262 			case 'O':
263 				/*
264 				** Locale modifiers of C99 and later.
265 				** The sequences
266 				**	%Ec %EC %Ex %EX %Ey %EY
267 				**	%Od %oe %OH %OI %Om %OM
268 				**	%OS %Ou %OU %OV %Ow %OW %Oy
269 				** are supposed to provide alternative
270 				** representations.
271 				*/
272 				if (Ealternative || Oalternative)
273 					break;
274 				Oalternative++;
275 				goto label;
276 			case 'e':
277 				pt = _conv(t->tm_mday,
278 				    fmt_padding[PAD_FMT_SDAYOFMONTH][PadIndex],
279 				    pt, ptlim, loc);
280 				continue;
281 			case 'F':
282 				pt = _fmt(sp, "%Y-%m-%d", t, pt, ptlim, warnp,
283 				    loc);
284 				continue;
285 			case 'H':
286 				pt = _conv(t->tm_hour,
287 				    fmt_padding[PAD_FMT_HMS][PadIndex],
288 				    pt, ptlim, loc);
289 				continue;
290 			case 'I':
291 				pt = _conv((t->tm_hour % 12) ?
292 				    (t->tm_hour % 12) : 12,
293 				    fmt_padding[PAD_FMT_HMS][PadIndex],
294 				    pt, ptlim, loc);
295 				continue;
296 			case 'j':
297 				pt = _conv(t->tm_yday + 1,
298 				    fmt_padding[PAD_FMT_DAYOFYEAR][PadIndex],
299 				    pt, ptlim, loc);
300 				continue;
301 			case 'k':
302 				/*
303 				** This used to be...
304 				**	_conv(t->tm_hour % 12 ?
305 				**		t->tm_hour % 12 : 12, 2, ' ');
306 				** ...and has been changed to the below to
307 				** match SunOS 4.1.1 and Arnold Robbins'
308 				** strftime version 3.0. That is, "%k" and
309 				** "%l" have been swapped.
310 				** (ado, 1993-05-24)
311 				*/
312 				pt = _conv(t->tm_hour,
313 				    fmt_padding[PAD_FMT_SHMS][PadIndex],
314 				    pt, ptlim, loc);
315 				continue;
316 #ifdef KITCHEN_SINK
317 			case 'K':
318 				/*
319 				** After all this time, still unclaimed!
320 				*/
321 				pt = _add("kitchen sink", pt, ptlim);
322 				continue;
323 #endif /* defined KITCHEN_SINK */
324 			case 'l':
325 				/*
326 				** This used to be...
327 				**	_conv(t->tm_hour, 2, ' ');
328 				** ...and has been changed to the below to
329 				** match SunOS 4.1.1 and Arnold Robbin's
330 				** strftime version 3.0. That is, "%k" and
331 				** "%l" have been swapped.
332 				** (ado, 1993-05-24)
333 				*/
334 				pt = _conv((t->tm_hour % 12) ?
335 					(t->tm_hour % 12) : 12,
336 					fmt_padding[PAD_FMT_SHMS][PadIndex],
337 					pt, ptlim, loc);
338 				continue;
339 			case 'M':
340 				pt = _conv(t->tm_min,
341 				    fmt_padding[PAD_FMT_HMS][PadIndex],
342 				    pt, ptlim, loc);
343 				continue;
344 			case 'm':
345 				pt = _conv(t->tm_mon + 1,
346 				    fmt_padding[PAD_FMT_MONTH][PadIndex],
347 				    pt, ptlim, loc);
348 				continue;
349 			case 'n':
350 				pt = _add("\n", pt, ptlim);
351 				continue;
352 			case 'p':
353 				pt = _add((t->tm_hour >= (HOURSPERDAY / 2)) ?
354 					tptr->am_pm[1] :
355 					tptr->am_pm[0],
356 					pt, ptlim);
357 				continue;
358 			case 'R':
359 				pt = _fmt(sp, "%H:%M", t, pt, ptlim, warnp,
360 				    loc);
361 				continue;
362 			case 'r':
363 				pt = _fmt(sp, tptr->t_fmt_ampm, t,
364 				    pt, ptlim, warnp, loc);
365 				continue;
366 			case 'S':
367 				pt = _conv(t->tm_sec,
368 				    fmt_padding[PAD_FMT_HMS][PadIndex],
369 				    pt, ptlim, loc);
370 				continue;
371 			case 's':
372 				{
373 					struct tm	tm;
374 					char		buf[INT_STRLEN_MAXIMUM(
375 								time_t) + 1];
376 					time_t		mkt;
377 
378 					tm.tm_sec = t->tm_sec;
379 					tm.tm_min = t->tm_min;
380 					tm.tm_hour = t->tm_hour;
381 					tm.tm_mday = t->tm_mday;
382 					tm.tm_mon = t->tm_mon;
383 					tm.tm_year = t->tm_year;
384 					tm.tm_isdst = t->tm_isdst;
385 #if defined TM_GMTOFF && ! UNINIT_TRAP
386 					tm.TM_GMTOFF = t->TM_GMTOFF;
387 #endif
388 					mkt = mktime_z(sp, &tm);
389 					/* If mktime fails, %s expands to the
390 					   value of (time_t) -1 as a failure
391 					   marker; this is better in practice
392 					   than strftime failing.  */
393 					/* CONSTCOND */
394 					if (TYPE_SIGNED(time_t)) {
395 						intmax_t n = mkt;
396 						(void)snprintf(buf, sizeof(buf),
397 						    "%"PRIdMAX, n);
398 					} else {
399 						/*LINTED possibly unreached*/
400 						uintmax_t n = mkt;
401 						(void)snprintf(buf, sizeof(buf),
402 						    "%"PRIuMAX, n);
403 					}
404 					pt = _add(buf, pt, ptlim);
405 				}
406 				continue;
407 			case 'T':
408 				pt = _fmt(sp, "%H:%M:%S", t, pt, ptlim, warnp,
409 				    loc);
410 				continue;
411 			case 't':
412 				pt = _add("\t", pt, ptlim);
413 				continue;
414 			case 'U':
415 				pt = _conv((t->tm_yday + DAYSPERWEEK -
416 				    t->tm_wday) / DAYSPERWEEK,
417 				    fmt_padding[PAD_FMT_WEEKOFYEAR][PadIndex],
418 				    pt, ptlim, loc);
419 				continue;
420 			case 'u':
421 				/*
422 				** From Arnold Robbins' strftime version 3.0:
423 				** "ISO 8601: Weekday as a decimal number
424 				** [1 (Monday) - 7]"
425 				** (ado, 1993-05-24)
426 				*/
427 				pt = _conv((t->tm_wday == 0) ?
428 					DAYSPERWEEK : t->tm_wday,
429 					"%d", pt, ptlim, loc);
430 				continue;
431 			case 'V':	/* ISO 8601 week number */
432 			case 'G':	/* ISO 8601 year (four digits) */
433 			case 'g':	/* ISO 8601 year (two digits) */
434 /*
435 ** From Arnold Robbins' strftime version 3.0: "the week number of the
436 ** year (the first Monday as the first day of week 1) as a decimal number
437 ** (01-53)."
438 ** (ado, 1993-05-24)
439 **
440 ** From <https://www.cl.cam.ac.uk/~mgk25/iso-time.html> by Markus Kuhn:
441 ** "Week 01 of a year is per definition the first week which has the
442 ** Thursday in this year, which is equivalent to the week which contains
443 ** the fourth day of January. In other words, the first week of a new year
444 ** is the week which has the majority of its days in the new year. Week 01
445 ** might also contain days from the previous year and the week before week
446 ** 01 of a year is the last week (52 or 53) of the previous year even if
447 ** it contains days from the new year. A week starts with Monday (day 1)
448 ** and ends with Sunday (day 7). For example, the first week of the year
449 ** 1997 lasts from 1996-12-30 to 1997-01-05..."
450 ** (ado, 1996-01-02)
451 */
452 				{
453 					int	year;
454 					int	base;
455 					int	yday;
456 					int	wday;
457 					int	w;
458 
459 					year = t->tm_year;
460 					base = TM_YEAR_BASE;
461 					yday = t->tm_yday;
462 					wday = t->tm_wday;
463 					for ( ; ; ) {
464 						int	len;
465 						int	bot;
466 						int	top;
467 
468 						len = isleap_sum(year, base) ?
469 							DAYSPERLYEAR :
470 							DAYSPERNYEAR;
471 						/*
472 						** What yday (-3 ... 3) does
473 						** the ISO year begin on?
474 						*/
475 						bot = ((yday + 11 - wday) %
476 							DAYSPERWEEK) - 3;
477 						/*
478 						** What yday does the NEXT
479 						** ISO year begin on?
480 						*/
481 						top = bot -
482 							(len % DAYSPERWEEK);
483 						if (top < -3)
484 							top += DAYSPERWEEK;
485 						top += len;
486 						if (yday >= top) {
487 							++base;
488 							w = 1;
489 							break;
490 						}
491 						if (yday >= bot) {
492 							w = 1 + ((yday - bot) /
493 								DAYSPERWEEK);
494 							break;
495 						}
496 						--base;
497 						yday += isleap_sum(year, base) ?
498 							DAYSPERLYEAR :
499 							DAYSPERNYEAR;
500 					}
501 #ifdef XPG4_1994_04_09
502 					if ((w == 52 &&
503 						t->tm_mon == TM_JANUARY) ||
504 						(w == 1 &&
505 						t->tm_mon == TM_DECEMBER))
506 							w = 53;
507 #endif /* defined XPG4_1994_04_09 */
508 					if (*format == 'V')
509 						pt = _conv(w,
510 						    fmt_padding[
511 						    PAD_FMT_WEEKOFYEAR][
512 						    PadIndex], pt, ptlim, loc);
513 					else if (*format == 'g') {
514 						*warnp = IN_ALL;
515 						pt = _yconv(year, base,
516 							false, true,
517 							pt, ptlim, loc);
518 					} else	pt = _yconv(year, base,
519 							true, true,
520 							pt, ptlim, loc);
521 				}
522 				continue;
523 			case 'v':
524 				/*
525 				** From Arnold Robbins' strftime version 3.0:
526 				** "date as dd-bbb-YYYY"
527 				** (ado, 1993-05-24)
528 				*/
529 				pt = _fmt(sp, "%e-%b-%Y", t, pt, ptlim, warnp,
530 				    loc);
531 				continue;
532 			case 'W':
533 				pt = _conv((t->tm_yday + DAYSPERWEEK -
534 				    (t->tm_wday ?
535 				    (t->tm_wday - 1) :
536 				    (DAYSPERWEEK - 1))) / DAYSPERWEEK,
537 				    fmt_padding[PAD_FMT_WEEKOFYEAR][PadIndex],
538 				    pt, ptlim, loc);
539 				continue;
540 			case 'w':
541 				pt = _conv(t->tm_wday, "%d", pt, ptlim, loc);
542 				continue;
543 			case 'X':
544 				pt = _fmt(sp, tptr->t_fmt, t, pt,
545 				    ptlim, warnp, loc);
546 				continue;
547 			case 'x':
548 				{
549 				enum warn warn2 = IN_SOME;
550 
551 				pt = _fmt(sp, tptr->d_fmt, t, pt,
552 				    ptlim, &warn2, loc);
553 				if (warn2 == IN_ALL)
554 					warn2 = IN_THIS;
555 				if (warn2 > *warnp)
556 					*warnp = warn2;
557 				}
558 				continue;
559 			case 'y':
560 				*warnp = IN_ALL;
561 				pt = _yconv(t->tm_year, TM_YEAR_BASE,
562 					false, true,
563 					pt, ptlim, loc);
564 				continue;
565 			case 'Y':
566 				pt = _yconv(t->tm_year, TM_YEAR_BASE,
567 					true, true,
568 					pt, ptlim, loc);
569 				continue;
570 			case 'Z':
571 #ifdef TM_ZONE
572 				pt = _add(t->TM_ZONE, pt, ptlim);
573 #elif HAVE_TZNAME
574 				if (t->tm_isdst >= 0) {
575 					int oerrno = errno, dst = t->tm_isdst;
576 					const char *z =
577 					    tzgetname(sp, dst);
578 					if (z == NULL)
579 						z = tzgetname(sp, !dst);
580 					if (z != NULL)
581 						pt = _add(z, pt, ptlim);
582 					errno = oerrno;
583 				}
584 #endif
585 				/*
586 				** C99 and later say that %Z must be
587 				** replaced by the empty string if the
588 				** time zone abbreviation is not
589 				** determinable.
590 				*/
591 				continue;
592 			case 'z':
593 #if defined TM_GMTOFF || USG_COMPAT || ALTZONE
594 				{
595 				long		diff;
596 				char const *	sign;
597 				bool negative;
598 
599 				if (t->tm_isdst < 0)
600 					continue;
601 # ifdef TM_GMTOFF
602 				diff = (int)t->TM_GMTOFF;
603 # else
604 				/*
605 				** C99 and later say that the UT offset must
606 				** be computed by looking only at
607 				** tm_isdst. This requirement is
608 				** incorrect, since it means the code
609 				** must rely on magic (in this case
610 				** altzone and timezone), and the
611 				** magic might not have the correct
612 				** offset. Doing things correctly is
613 				** tricky and requires disobeying the standard;
614 				** see GNU C strftime for details.
615 				** For now, punt and conform to the
616 				** standard, even though it's incorrect.
617 				**
618 				** C99 and later say that %z must be replaced by
619 				** the empty string if the time zone is not
620 				** determinable, so output nothing if the
621 				** appropriate variables are not available.
622 				*/
623 #  ifndef STD_INSPIRED
624 				if (t->tm_isdst == 0)
625 #   if USG_COMPAT
626 					diff = -timezone;
627 #   else
628 					continue;
629 #   endif
630 				else
631 #   if ALTZONE
632 					diff = -altzone;
633 #   else
634 					continue;
635 #   endif
636 #  else
637 				{
638 					struct tm tmp;
639 					time_t lct, gct;
640 
641 					/*
642 					** Get calendar time from t
643 					** being treated as local.
644 					*/
645 					tmp = *t; /* mktime discards const */
646 					lct = mktime_z(sp, &tmp);
647 
648 					if (lct == (time_t)-1)
649 						continue;
650 
651 					/*
652 					** Get calendar time from t
653 					** being treated as GMT.
654 					**/
655 					tmp = *t; /* mktime discards const */
656 					gct = timegm(&tmp);
657 
658 					if (gct == (time_t)-1)
659 						continue;
660 
661 					/* LINTED difference will fit int */
662 					diff = (intmax_t)gct - (intmax_t)lct;
663 				}
664 #  endif
665 # endif
666 				negative = diff < 0;
667 				if (diff == 0) {
668 # ifdef TM_ZONE
669 				  negative = t->TM_ZONE[0] == '-';
670 # else
671 				  negative = t->tm_isdst < 0;
672 #  if HAVE_TZNAME
673 				  if (tzname[t->tm_isdst != 0][0] == '-')
674 				    negative = true;
675 #  endif
676 # endif
677 				}
678 				if (negative) {
679 					sign = "-";
680 					diff = -diff;
681 				} else	sign = "+";
682 				pt = _add(sign, pt, ptlim);
683 				diff /= SECSPERMIN;
684 				diff = (diff / MINSPERHOUR) * 100 +
685 					(diff % MINSPERHOUR);
686 				_DIAGASSERT(__type_fit(int, diff));
687 				pt = _conv((int)diff,
688 				    fmt_padding[PAD_FMT_YEAR][PadIndex],
689 				    pt, ptlim, loc);
690 				}
691 #endif
692 				continue;
693 			case '+':
694 #ifdef notyet
695 				/* XXX: no date_fmt in _TimeLocale */
696 				pt = _fmt(sp, tptr->date_fmt, t,
697 				    pt, ptlim, warnp, loc);
698 #else
699 				pt = _fmt(sp, "%a %b %e %H:%M:%S %Z %Y", t,
700 				    pt, ptlim, warnp, loc);
701 #endif
702 				continue;
703 			case '-':
704 				if (PadIndex != PAD_DEFAULT)
705 					break;
706 				PadIndex = PAD_LESS;
707 				goto label;
708 			case '_':
709 				if (PadIndex != PAD_DEFAULT)
710 					break;
711 				PadIndex = PAD_SPACE;
712 				goto label;
713 			case '0':
714 				if (PadIndex != PAD_DEFAULT)
715 					break;
716 				PadIndex = PAD_ZERO;
717 				goto label;
718 			case '%':
719 			/*
720 			** X311J/88-090 (4.12.3.5): if conversion char is
721 			** undefined, behavior is undefined. Print out the
722 			** character itself as printf(3) also does.
723 			*/
724 			default:
725 				break;
726 			}
727 		}
728 		if (pt == ptlim)
729 			break;
730 		*pt++ = *format;
731 	}
732 	return pt;
733 }
734 
735 size_t
736 strftime(char *restrict s, size_t maxsize, char const *restrict format,
737 	 struct tm const *restrict t)
738 {
739 	size_t r;
740 
741 	rwlock_wrlock(&__lcl_lock);
742 	tzset_unlocked();
743 	r = strftime_z(__lclptr, s, maxsize, format, t);
744 	rwlock_unlock(&__lcl_lock);
745 
746 	return r;
747 }
748 
749 size_t
750 strftime_l(char * __restrict s, size_t maxsize, const char * __restrict format,
751     const struct tm * __restrict t, locale_t loc)
752 {
753 	size_t r;
754 
755 	rwlock_wrlock(&__lcl_lock);
756 	tzset_unlocked();
757 	r = strftime_lz(__lclptr, s, maxsize, format, t, loc);
758 	rwlock_unlock(&__lcl_lock);
759 
760 	return r;
761 }
762 
763 static char *
764 _conv(int n, const char *format, char *pt, const char *ptlim, locale_t loc)
765 {
766 	char	buf[INT_STRLEN_MAXIMUM(int) + 1];
767 
768 	(void) snprintf_l(buf, sizeof(buf), loc, format, n);
769 	return _add(buf, pt, ptlim);
770 }
771 
772 static char *
773 _add(const char *str, char *pt, const char *ptlim)
774 {
775 	while (pt < ptlim && (*pt = *str++) != '\0')
776 		++pt;
777 	return pt;
778 }
779 
780 /*
781 ** POSIX and the C Standard are unclear or inconsistent about
782 ** what %C and %y do if the year is negative or exceeds 9999.
783 ** Use the convention that %C concatenated with %y yields the
784 ** same output as %Y, and that %Y contains at least 4 bytes,
785 ** with more only if necessary.
786 */
787 
788 static char *
789 _yconv(int a, int b, bool convert_top, bool convert_yy,
790     char *pt, const char * ptlim, locale_t loc)
791 {
792 	int	lead;
793 	int	trail;
794 
795 	int DIVISOR = 100;
796 	trail = a % DIVISOR + b % DIVISOR;
797 	lead = a / DIVISOR + b / DIVISOR + trail / DIVISOR;
798 	trail %= DIVISOR;
799 	if (trail < 0 && lead > 0) {
800 		trail += DIVISOR;
801 		--lead;
802 	} else if (lead < 0 && trail > 0) {
803 		trail -= DIVISOR;
804 		++lead;
805 	}
806 	if (convert_top) {
807 		if (lead == 0 && trail < 0)
808 			pt = _add("-0", pt, ptlim);
809 		else	pt = _conv(lead, "%02d", pt, ptlim, loc);
810 	}
811 	if (convert_yy)
812 		pt = _conv(((trail < 0) ? -trail : trail), "%02d", pt, ptlim,
813 		    loc);
814 	return pt;
815 }
816