1 /* Convert a broken-down timestamp to a string.  */
2 
3 /*
4  * Copyright 1989 The Regents of the University of California.
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *	  notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *	  notice, this list of conditions and the following disclaimer in the
14  *	  documentation and/or other materials provided with the distribution.
15  * 3. Neither the name of the University nor the names of its contributors
16  *	  may be used to endorse or promote products derived from this software
17  *	  without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
23  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29  * SUCH DAMAGE.
30  */
31 
32 /*
33  * Based on the UCB version with the copyright notice appearing above.
34  *
35  * This is ANSIish only when "multibyte character == plain character".
36  *
37  * IDENTIFICATION
38  *	  src/timezone/strftime.c
39  */
40 
41 #include "postgres.h"
42 
43 #include <fcntl.h>
44 
45 #include "private.h"
46 
47 
48 struct lc_time_T
49 {
50 	const char *mon[MONSPERYEAR];
51 	const char *month[MONSPERYEAR];
52 	const char *wday[DAYSPERWEEK];
53 	const char *weekday[DAYSPERWEEK];
54 	const char *X_fmt;
55 	const char *x_fmt;
56 	const char *c_fmt;
57 	const char *am;
58 	const char *pm;
59 	const char *date_fmt;
60 };
61 
62 #define Locale	(&C_time_locale)
63 
64 static const struct lc_time_T C_time_locale = {
65 	{
66 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
67 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
68 	}, {
69 		"January", "February", "March", "April", "May", "June",
70 		"July", "August", "September", "October", "November", "December"
71 	}, {
72 		"Sun", "Mon", "Tue", "Wed",
73 		"Thu", "Fri", "Sat"
74 	}, {
75 		"Sunday", "Monday", "Tuesday", "Wednesday",
76 		"Thursday", "Friday", "Saturday"
77 	},
78 
79 	/* X_fmt */
80 	"%H:%M:%S",
81 
82 	/*
83 	 * x_fmt
84 	 *
85 	 * C99 and later require this format. Using just numbers (as here) makes
86 	 * Quakers happier; it's also compatible with SVR4.
87 	 */
88 	"%m/%d/%y",
89 
90 	/*
91 	 * c_fmt
92 	 *
93 	 * C99 and later require this format. Previously this code used "%D %X",
94 	 * but we now conform to C99. Note that "%a %b %d %H:%M:%S %Y" is used by
95 	 * Solaris 2.3.
96 	 */
97 	"%a %b %e %T %Y",
98 
99 	/* am */
100 	"AM",
101 
102 	/* pm */
103 	"PM",
104 
105 	/* date_fmt */
106 	"%a %b %e %H:%M:%S %Z %Y"
107 };
108 
109 enum warn
110 {
111 	IN_NONE, IN_SOME, IN_THIS, IN_ALL
112 };
113 
114 static char *_add(const char *, char *, const char *);
115 static char *_conv(int, const char *, char *, const char *);
116 static char *_fmt(const char *, const struct pg_tm *, char *, const char *,
117 				  enum warn *);
118 static char *_yconv(int, int, bool, bool, char *, char const *);
119 
120 
121 /*
122  * Convert timestamp t to string s, a caller-allocated buffer of size maxsize,
123  * using the given format pattern.
124  *
125  * See also timestamptz_to_str.
126  */
127 size_t
pg_strftime(char * s,size_t maxsize,const char * format,const struct pg_tm * t)128 pg_strftime(char *s, size_t maxsize, const char *format, const struct pg_tm *t)
129 {
130 	char	   *p;
131 	int			saved_errno = errno;
132 	enum warn	warn = IN_NONE;
133 
134 	p = _fmt(format, t, s, s + maxsize, &warn);
135 	if (!p)
136 	{
137 		errno = EOVERFLOW;
138 		return 0;
139 	}
140 	if (p == s + maxsize)
141 	{
142 		errno = ERANGE;
143 		return 0;
144 	}
145 	*p = '\0';
146 	errno = saved_errno;
147 	return p - s;
148 }
149 
150 static char *
_fmt(const char * format,const struct pg_tm * t,char * pt,const char * ptlim,enum warn * warnp)151 _fmt(const char *format, const struct pg_tm *t, char *pt,
152 	 const char *ptlim, enum warn *warnp)
153 {
154 	for (; *format; ++format)
155 	{
156 		if (*format == '%')
157 		{
158 	label:
159 			switch (*++format)
160 			{
161 				case '\0':
162 					--format;
163 					break;
164 				case 'A':
165 					pt = _add((t->tm_wday < 0 ||
166 							   t->tm_wday >= DAYSPERWEEK) ?
167 							  "?" : Locale->weekday[t->tm_wday],
168 							  pt, ptlim);
169 					continue;
170 				case 'a':
171 					pt = _add((t->tm_wday < 0 ||
172 							   t->tm_wday >= DAYSPERWEEK) ?
173 							  "?" : Locale->wday[t->tm_wday],
174 							  pt, ptlim);
175 					continue;
176 				case 'B':
177 					pt = _add((t->tm_mon < 0 ||
178 							   t->tm_mon >= MONSPERYEAR) ?
179 							  "?" : Locale->month[t->tm_mon],
180 							  pt, ptlim);
181 					continue;
182 				case 'b':
183 				case 'h':
184 					pt = _add((t->tm_mon < 0 ||
185 							   t->tm_mon >= MONSPERYEAR) ?
186 							  "?" : Locale->mon[t->tm_mon],
187 							  pt, ptlim);
188 					continue;
189 				case 'C':
190 
191 					/*
192 					 * %C used to do a... _fmt("%a %b %e %X %Y", t);
193 					 * ...whereas now POSIX 1003.2 calls for something
194 					 * completely different. (ado, 1993-05-24)
195 					 */
196 					pt = _yconv(t->tm_year, TM_YEAR_BASE,
197 								true, false, pt, ptlim);
198 					continue;
199 				case 'c':
200 					{
201 						enum warn	warn2 = IN_SOME;
202 
203 						pt = _fmt(Locale->c_fmt, t, pt, ptlim, &warn2);
204 						if (warn2 == IN_ALL)
205 							warn2 = IN_THIS;
206 						if (warn2 > *warnp)
207 							*warnp = warn2;
208 					}
209 					continue;
210 				case 'D':
211 					pt = _fmt("%m/%d/%y", t, pt, ptlim, warnp);
212 					continue;
213 				case 'd':
214 					pt = _conv(t->tm_mday, "%02d", pt, ptlim);
215 					continue;
216 				case 'E':
217 				case 'O':
218 
219 					/*
220 					 * Locale modifiers of C99 and later. The sequences %Ec
221 					 * %EC %Ex %EX %Ey %EY %Od %oe %OH %OI %Om %OM %OS %Ou %OU
222 					 * %OV %Ow %OW %Oy are supposed to provide alternative
223 					 * representations.
224 					 */
225 					goto label;
226 				case 'e':
227 					pt = _conv(t->tm_mday, "%2d", pt, ptlim);
228 					continue;
229 				case 'F':
230 					pt = _fmt("%Y-%m-%d", t, pt, ptlim, warnp);
231 					continue;
232 				case 'H':
233 					pt = _conv(t->tm_hour, "%02d", pt, ptlim);
234 					continue;
235 				case 'I':
236 					pt = _conv((t->tm_hour % 12) ?
237 							   (t->tm_hour % 12) : 12,
238 							   "%02d", pt, ptlim);
239 					continue;
240 				case 'j':
241 					pt = _conv(t->tm_yday + 1, "%03d", pt, ptlim);
242 					continue;
243 				case 'k':
244 
245 					/*
246 					 * This used to be... _conv(t->tm_hour % 12 ? t->tm_hour %
247 					 * 12 : 12, 2, ' '); ...and has been changed to the below
248 					 * to match SunOS 4.1.1 and Arnold Robbins' strftime
249 					 * version 3.0. That is, "%k" and "%l" have been swapped.
250 					 * (ado, 1993-05-24)
251 					 */
252 					pt = _conv(t->tm_hour, "%2d", pt, ptlim);
253 					continue;
254 #ifdef KITCHEN_SINK
255 				case 'K':
256 
257 					/*
258 					 * After all this time, still unclaimed!
259 					 */
260 					pt = _add("kitchen sink", pt, ptlim);
261 					continue;
262 #endif							/* defined KITCHEN_SINK */
263 				case 'l':
264 
265 					/*
266 					 * This used to be... _conv(t->tm_hour, 2, ' '); ...and
267 					 * has been changed to the below to match SunOS 4.1.1 and
268 					 * Arnold Robbin's strftime version 3.0. That is, "%k" and
269 					 * "%l" have been swapped. (ado, 1993-05-24)
270 					 */
271 					pt = _conv((t->tm_hour % 12) ?
272 							   (t->tm_hour % 12) : 12,
273 							   "%2d", pt, ptlim);
274 					continue;
275 				case 'M':
276 					pt = _conv(t->tm_min, "%02d", pt, ptlim);
277 					continue;
278 				case 'm':
279 					pt = _conv(t->tm_mon + 1, "%02d", pt, ptlim);
280 					continue;
281 				case 'n':
282 					pt = _add("\n", pt, ptlim);
283 					continue;
284 				case 'p':
285 					pt = _add((t->tm_hour >= (HOURSPERDAY / 2)) ?
286 							  Locale->pm :
287 							  Locale->am,
288 							  pt, ptlim);
289 					continue;
290 				case 'R':
291 					pt = _fmt("%H:%M", t, pt, ptlim, warnp);
292 					continue;
293 				case 'r':
294 					pt = _fmt("%I:%M:%S %p", t, pt, ptlim, warnp);
295 					continue;
296 				case 'S':
297 					pt = _conv(t->tm_sec, "%02d", pt, ptlim);
298 					continue;
299 				case 'T':
300 					pt = _fmt("%H:%M:%S", t, pt, ptlim, warnp);
301 					continue;
302 				case 't':
303 					pt = _add("\t", pt, ptlim);
304 					continue;
305 				case 'U':
306 					pt = _conv((t->tm_yday + DAYSPERWEEK -
307 								t->tm_wday) / DAYSPERWEEK,
308 							   "%02d", pt, ptlim);
309 					continue;
310 				case 'u':
311 
312 					/*
313 					 * From Arnold Robbins' strftime version 3.0: "ISO 8601:
314 					 * Weekday as a decimal number [1 (Monday) - 7]" (ado,
315 					 * 1993-05-24)
316 					 */
317 					pt = _conv((t->tm_wday == 0) ?
318 							   DAYSPERWEEK : t->tm_wday,
319 							   "%d", pt, ptlim);
320 					continue;
321 				case 'V':		/* ISO 8601 week number */
322 				case 'G':		/* ISO 8601 year (four digits) */
323 				case 'g':		/* ISO 8601 year (two digits) */
324 /*
325  * From Arnold Robbins' strftime version 3.0: "the week number of the
326  * year (the first Monday as the first day of week 1) as a decimal number
327  * (01-53)."
328  * (ado, 1993-05-24)
329  *
330  * From <https://www.cl.cam.ac.uk/~mgk25/iso-time.html> by Markus Kuhn:
331  * "Week 01 of a year is per definition the first week which has the
332  * Thursday in this year, which is equivalent to the week which contains
333  * the fourth day of January. In other words, the first week of a new year
334  * is the week which has the majority of its days in the new year. Week 01
335  * might also contain days from the previous year and the week before week
336  * 01 of a year is the last week (52 or 53) of the previous year even if
337  * it contains days from the new year. A week starts with Monday (day 1)
338  * and ends with Sunday (day 7). For example, the first week of the year
339  * 1997 lasts from 1996-12-30 to 1997-01-05..."
340  * (ado, 1996-01-02)
341  */
342 					{
343 						int			year;
344 						int			base;
345 						int			yday;
346 						int			wday;
347 						int			w;
348 
349 						year = t->tm_year;
350 						base = TM_YEAR_BASE;
351 						yday = t->tm_yday;
352 						wday = t->tm_wday;
353 						for (;;)
354 						{
355 							int			len;
356 							int			bot;
357 							int			top;
358 
359 							len = isleap_sum(year, base) ?
360 								DAYSPERLYEAR :
361 								DAYSPERNYEAR;
362 
363 							/*
364 							 * What yday (-3 ... 3) does the ISO year begin
365 							 * on?
366 							 */
367 							bot = ((yday + 11 - wday) %
368 								   DAYSPERWEEK) - 3;
369 
370 							/*
371 							 * What yday does the NEXT ISO year begin on?
372 							 */
373 							top = bot -
374 								(len % DAYSPERWEEK);
375 							if (top < -3)
376 								top += DAYSPERWEEK;
377 							top += len;
378 							if (yday >= top)
379 							{
380 								++base;
381 								w = 1;
382 								break;
383 							}
384 							if (yday >= bot)
385 							{
386 								w = 1 + ((yday - bot) /
387 										 DAYSPERWEEK);
388 								break;
389 							}
390 							--base;
391 							yday += isleap_sum(year, base) ?
392 								DAYSPERLYEAR :
393 								DAYSPERNYEAR;
394 						}
395 						if (*format == 'V')
396 							pt = _conv(w, "%02d",
397 									   pt, ptlim);
398 						else if (*format == 'g')
399 						{
400 							*warnp = IN_ALL;
401 							pt = _yconv(year, base,
402 										false, true,
403 										pt, ptlim);
404 						}
405 						else
406 							pt = _yconv(year, base,
407 										true, true,
408 										pt, ptlim);
409 					}
410 					continue;
411 				case 'v':
412 
413 					/*
414 					 * From Arnold Robbins' strftime version 3.0: "date as
415 					 * dd-bbb-YYYY" (ado, 1993-05-24)
416 					 */
417 					pt = _fmt("%e-%b-%Y", t, pt, ptlim, warnp);
418 					continue;
419 				case 'W':
420 					pt = _conv((t->tm_yday + DAYSPERWEEK -
421 								(t->tm_wday ?
422 								 (t->tm_wday - 1) :
423 								 (DAYSPERWEEK - 1))) / DAYSPERWEEK,
424 							   "%02d", pt, ptlim);
425 					continue;
426 				case 'w':
427 					pt = _conv(t->tm_wday, "%d", pt, ptlim);
428 					continue;
429 				case 'X':
430 					pt = _fmt(Locale->X_fmt, t, pt, ptlim, warnp);
431 					continue;
432 				case 'x':
433 					{
434 						enum warn	warn2 = IN_SOME;
435 
436 						pt = _fmt(Locale->x_fmt, t, pt, ptlim, &warn2);
437 						if (warn2 == IN_ALL)
438 							warn2 = IN_THIS;
439 						if (warn2 > *warnp)
440 							*warnp = warn2;
441 					}
442 					continue;
443 				case 'y':
444 					*warnp = IN_ALL;
445 					pt = _yconv(t->tm_year, TM_YEAR_BASE,
446 								false, true,
447 								pt, ptlim);
448 					continue;
449 				case 'Y':
450 					pt = _yconv(t->tm_year, TM_YEAR_BASE,
451 								true, true,
452 								pt, ptlim);
453 					continue;
454 				case 'Z':
455 					if (t->tm_zone != NULL)
456 						pt = _add(t->tm_zone, pt, ptlim);
457 
458 					/*
459 					 * C99 and later say that %Z must be replaced by the empty
460 					 * string if the time zone abbreviation is not
461 					 * determinable.
462 					 */
463 					continue;
464 				case 'z':
465 					{
466 						long		diff;
467 						char const *sign;
468 						bool		negative;
469 
470 						if (t->tm_isdst < 0)
471 							continue;
472 						diff = t->tm_gmtoff;
473 						negative = diff < 0;
474 						if (diff == 0)
475 						{
476 							if (t->tm_zone != NULL)
477 								negative = t->tm_zone[0] == '-';
478 						}
479 						if (negative)
480 						{
481 							sign = "-";
482 							diff = -diff;
483 						}
484 						else
485 							sign = "+";
486 						pt = _add(sign, pt, ptlim);
487 						diff /= SECSPERMIN;
488 						diff = (diff / MINSPERHOUR) * 100 +
489 							(diff % MINSPERHOUR);
490 						pt = _conv(diff, "%04d", pt, ptlim);
491 					}
492 					continue;
493 				case '+':
494 					pt = _fmt(Locale->date_fmt, t, pt, ptlim,
495 							  warnp);
496 					continue;
497 				case '%':
498 
499 					/*
500 					 * X311J/88-090 (4.12.3.5): if conversion char is
501 					 * undefined, behavior is undefined. Print out the
502 					 * character itself as printf(3) also does.
503 					 */
504 				default:
505 					break;
506 			}
507 		}
508 		if (pt == ptlim)
509 			break;
510 		*pt++ = *format;
511 	}
512 	return pt;
513 }
514 
515 static char *
_conv(int n,const char * format,char * pt,const char * ptlim)516 _conv(int n, const char *format, char *pt, const char *ptlim)
517 {
518 	char		buf[INT_STRLEN_MAXIMUM(int) + 1];
519 
520 	sprintf(buf, format, n);
521 	return _add(buf, pt, ptlim);
522 }
523 
524 static char *
_add(const char * str,char * pt,const char * ptlim)525 _add(const char *str, char *pt, const char *ptlim)
526 {
527 	while (pt < ptlim && (*pt = *str++) != '\0')
528 		++pt;
529 	return pt;
530 }
531 
532 /*
533  * POSIX and the C Standard are unclear or inconsistent about
534  * what %C and %y do if the year is negative or exceeds 9999.
535  * Use the convention that %C concatenated with %y yields the
536  * same output as %Y, and that %Y contains at least 4 bytes,
537  * with more only if necessary.
538  */
539 
540 static char *
_yconv(int a,int b,bool convert_top,bool convert_yy,char * pt,const char * ptlim)541 _yconv(int a, int b, bool convert_top, bool convert_yy,
542 	   char *pt, const char *ptlim)
543 {
544 	int			lead;
545 	int			trail;
546 
547 #define DIVISOR	100
548 	trail = a % DIVISOR + b % DIVISOR;
549 	lead = a / DIVISOR + b / DIVISOR + trail / DIVISOR;
550 	trail %= DIVISOR;
551 	if (trail < 0 && lead > 0)
552 	{
553 		trail += DIVISOR;
554 		--lead;
555 	}
556 	else if (lead < 0 && trail > 0)
557 	{
558 		trail -= DIVISOR;
559 		++lead;
560 	}
561 	if (convert_top)
562 	{
563 		if (lead == 0 && trail < 0)
564 			pt = _add("-0", pt, ptlim);
565 		else
566 			pt = _conv(lead, "%02d", pt, ptlim);
567 	}
568 	if (convert_yy)
569 		pt = _conv(((trail < 0) ? -trail : trail), "%02d", pt, ptlim);
570 	return pt;
571 }
572