From aa2387e2fd532954e88dfd8546ab894b9305123d Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Sat, 6 Feb 2016 23:11:28 -0500 Subject: [PATCH] Improve speed of timestamp/time/date output functions. It seems that sprintf(), at least in glibc's version, is unreasonably slow compared to hand-rolled code for printing integers. Replacing most uses of sprintf() in the datetime.c output functions with special-purpose code turns out to give more than a 2X speedup in COPY of a table with a single timestamp column; which is pretty impressive considering all the other logic in that code path. David Rowley and Andres Freund, reviewed by Peter Geoghegan and myself --- src/backend/utils/adt/datetime.c | 389 +++++++++++++++++++++---------- src/backend/utils/adt/numutils.c | 161 +++++++++++++ src/include/utils/builtins.h | 2 + 3 files changed, 424 insertions(+), 128 deletions(-) diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 90b1eb16e4..cdbf72cf69 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -43,8 +43,12 @@ static int DecodeTime(char *str, int fmask, int range, static const datetkn *datebsearch(const char *key, const datetkn *base, int nel); static int DecodeDate(char *str, int fmask, int *tmask, bool *is2digits, struct pg_tm * tm); -static void TrimTrailingZeros(char *str); -static void AppendSeconds(char *cp, int sec, fsec_t fsec, + +#ifndef HAVE_INT64_TIMESTAMP +static char *TrimTrailingZeros(char *str); +#endif /* HAVE_INT64_TIMESTAMP */ + +static char *AppendSeconds(char *cp, int sec, fsec_t fsec, int precision, bool fillzeros); static void AdjustFractSeconds(double frac, struct pg_tm * tm, fsec_t *fsec, int scale); @@ -398,57 +402,121 @@ GetCurrentTimeUsec(struct pg_tm * tm, fsec_t *fsec, int *tzp) /* TrimTrailingZeros() * ... resulting from printing numbers with full precision. * + * Returns a pointer to the new end of string. No NUL terminator is put + * there; callers are responsible for NUL terminating str themselves. + * * Before Postgres 8.4, this always left at least 2 fractional digits, * but conversations on the lists suggest this isn't desired * since showing '0.10' is misleading with values of precision(1). */ -static void +#ifndef HAVE_INT64_TIMESTAMP +static char * TrimTrailingZeros(char *str) { int len = strlen(str); while (len > 1 && *(str + len - 1) == '0' && *(str + len - 2) != '.') - { len--; - *(str + len) = '\0'; - } + return str + len; } +#endif /* HAVE_INT64_TIMESTAMP */ /* - * Append sections and fractional seconds (if any) at *cp. + * Append seconds and fractional seconds (if any) at *cp. + * * precision is the max number of fraction digits, fillzeros says to * pad to two integral-seconds digits. + * + * Returns a pointer to the new end of string. No NUL terminator is put + * there; callers are responsible for NUL terminating str themselves. + * * Note that any sign is stripped from the input seconds values. */ -static void +static char * AppendSeconds(char *cp, int sec, fsec_t fsec, int precision, bool fillzeros) { + Assert(precision >= 0); + +#ifdef HAVE_INT64_TIMESTAMP + /* fsec_t is just an int32 */ + + if (fillzeros) + cp = pg_ltostr_zeropad(cp, Abs(sec), 2); + else + cp = pg_ltostr(cp, Abs(sec)); + + if (fsec != 0) + { + int32 value = Abs(fsec); + char *end = &cp[precision + 1]; + bool gotnonzero = false; + + *cp++ = '.'; + + /* + * Append the fractional seconds part. Note that we don't want any + * trailing zeros here, so since we're building the number in reverse + * we'll skip appending zeros until we've output a non-zero digit. + */ + while (precision--) + { + int32 oldval = value; + int32 remainder; + + value /= 10; + remainder = oldval - value * 10; + + /* check if we got a non-zero */ + if (remainder) + gotnonzero = true; + + if (gotnonzero) + cp[precision] = '0' + remainder; + else + end = &cp[precision]; + } + + /* + * If we still have a non-zero value then precision must have not been + * enough to print the number. We punt the problem to pg_ltostr(), + * which will generate a correct answer in the minimum valid width. + */ + if (value) + return pg_ltostr(cp, Abs(fsec)); + + return end; + } + else + return cp; +#else + /* fsec_t is a double */ + if (fsec == 0) { if (fillzeros) - sprintf(cp, "%02d", abs(sec)); + return pg_ltostr_zeropad(cp, Abs(sec), 2); else - sprintf(cp, "%d", abs(sec)); + return pg_ltostr(cp, Abs(sec)); } else { -#ifdef HAVE_INT64_TIMESTAMP - if (fillzeros) - sprintf(cp, "%02d.%0*d", abs(sec), precision, (int) Abs(fsec)); - else - sprintf(cp, "%d.%0*d", abs(sec), precision, (int) Abs(fsec)); -#else if (fillzeros) sprintf(cp, "%0*.*f", precision + 3, precision, fabs(sec + fsec)); else sprintf(cp, "%.*f", precision, fabs(sec + fsec)); -#endif - TrimTrailingZeros(cp); + return TrimTrailingZeros(cp); } +#endif /* HAVE_INT64_TIMESTAMP */ } -/* Variant of above that's specialized to timestamp case */ -static void + +/* + * Variant of above that's specialized to timestamp case. + * + * Returns a pointer to the new end of string. No NUL terminator is put + * there; callers are responsible for NUL terminating str themselves. + */ +static char * AppendTimestampSeconds(char *cp, struct pg_tm * tm, fsec_t fsec) { /* @@ -459,7 +527,7 @@ AppendTimestampSeconds(char *cp, struct pg_tm * tm, fsec_t fsec) if (tm->tm_year <= 0) fsec = 0; #endif - AppendSeconds(cp, tm->tm_sec, fsec, MAX_TIMESTAMP_PRECISION, true); + return AppendSeconds(cp, tm->tm_sec, fsec, MAX_TIMESTAMP_PRECISION, true); } /* @@ -3831,9 +3899,12 @@ datebsearch(const char *key, const datetkn *base, int nel) } /* EncodeTimezone() - * Append representation of a numeric timezone offset to str. + * Copies representation of a numeric timezone offset to str. + * + * Returns a pointer to the new end of string. No NUL terminator is put + * there; callers are responsible for NUL terminating str themselves. */ -static void +static char * EncodeTimezone(char *str, int tz, int style) { int hour, @@ -3846,16 +3917,26 @@ EncodeTimezone(char *str, int tz, int style) hour = min / MINS_PER_HOUR; min -= hour * MINS_PER_HOUR; - str += strlen(str); /* TZ is negated compared to sign we wish to display ... */ *str++ = (tz <= 0 ? '+' : '-'); if (sec != 0) - sprintf(str, "%02d:%02d:%02d", hour, min, sec); + { + str = pg_ltostr_zeropad(str, hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, min, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, sec, 2); + } else if (min != 0 || style == USE_XSD_DATES) - sprintf(str, "%02d:%02d", hour, min); + { + str = pg_ltostr_zeropad(str, hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, min, 2); + } else - sprintf(str, "%02d", hour); + str = pg_ltostr_zeropad(str, hour, 2); + return str; } /* EncodeDateOnly() @@ -3871,48 +3952,70 @@ EncodeDateOnly(struct pg_tm * tm, int style, char *str) case USE_ISO_DATES: case USE_XSD_DATES: /* compatible with ISO date formats */ - if (tm->tm_year > 0) - sprintf(str, "%04d-%02d-%02d", - tm->tm_year, tm->tm_mon, tm->tm_mday); - else - sprintf(str, "%04d-%02d-%02d %s", - -(tm->tm_year - 1), tm->tm_mon, tm->tm_mday, "BC"); + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); break; case USE_SQL_DATES: /* compatible with Oracle/Ingres date formats */ if (DateOrder == DATEORDER_DMY) - sprintf(str, "%02d/%02d", tm->tm_mday, tm->tm_mon); + { + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = '/'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + } else - sprintf(str, "%02d/%02d", tm->tm_mon, tm->tm_mday); - if (tm->tm_year > 0) - sprintf(str + 5, "/%04d", tm->tm_year); - else - sprintf(str + 5, "/%04d %s", -(tm->tm_year - 1), "BC"); + { + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '/'; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + } + *str++ = '/'; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); break; case USE_GERMAN_DATES: /* German-style date format */ - sprintf(str, "%02d.%02d", tm->tm_mday, tm->tm_mon); - if (tm->tm_year > 0) - sprintf(str + 5, ".%04d", tm->tm_year); - else - sprintf(str + 5, ".%04d %s", -(tm->tm_year - 1), "BC"); + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = '.'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '.'; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); break; case USE_POSTGRES_DATES: default: /* traditional date-only style for Postgres */ if (DateOrder == DATEORDER_DMY) - sprintf(str, "%02d-%02d", tm->tm_mday, tm->tm_mon); + { + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + } else - sprintf(str, "%02d-%02d", tm->tm_mon, tm->tm_mday); - if (tm->tm_year > 0) - sprintf(str + 5, "-%04d", tm->tm_year); - else - sprintf(str + 5, "-%04d %s", -(tm->tm_year - 1), "BC"); + { + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + } + *str++ = '-'; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); break; } + + if (tm->tm_year <= 0) + { + memcpy(str, " BC", 3); /* Don't copy NUL */ + str += 3; + } + *str = '\0'; } @@ -3927,13 +4030,14 @@ EncodeDateOnly(struct pg_tm * tm, int style, char *str) void EncodeTimeOnly(struct pg_tm * tm, fsec_t fsec, bool print_tz, int tz, int style, char *str) { - sprintf(str, "%02d:%02d:", tm->tm_hour, tm->tm_min); - str += strlen(str); - - AppendSeconds(str, tm->tm_sec, fsec, MAX_TIME_PRECISION, true); - + str = pg_ltostr_zeropad(str, tm->tm_hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, tm->tm_min, 2); + *str++ = ':'; + str = AppendSeconds(str, tm->tm_sec, fsec, MAX_TIME_PRECISION, true); if (print_tz) - EncodeTimezone(str, tz, style); + str = EncodeTimezone(str, tz, style); + *str = '\0'; } @@ -3971,106 +4075,129 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, bool print_tz, int tz, const char case USE_ISO_DATES: case USE_XSD_DATES: /* Compatible with ISO-8601 date formats */ - - if (style == USE_ISO_DATES) - sprintf(str, "%04d-%02d-%02d %02d:%02d:", - (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), - tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min); - else - sprintf(str, "%04d-%02d-%02dT%02d:%02d:", - (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), - tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min); - - AppendTimestampSeconds(str + strlen(str), tm, fsec); - + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '-'; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = (style == USE_ISO_DATES) ? ' ' : 'T'; + str = pg_ltostr_zeropad(str, tm->tm_hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, tm->tm_min, 2); + *str++ = ':'; + str = AppendTimestampSeconds(str, tm, fsec); if (print_tz) - EncodeTimezone(str, tz, style); - - if (tm->tm_year <= 0) - sprintf(str + strlen(str), " BC"); + str = EncodeTimezone(str, tz, style); break; case USE_SQL_DATES: /* Compatible with Oracle/Ingres date formats */ - if (DateOrder == DATEORDER_DMY) - sprintf(str, "%02d/%02d", tm->tm_mday, tm->tm_mon); + { + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = '/'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + } else - sprintf(str, "%02d/%02d", tm->tm_mon, tm->tm_mday); - - sprintf(str + 5, "/%04d %02d:%02d:", - (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), - tm->tm_hour, tm->tm_min); - - AppendTimestampSeconds(str + strlen(str), tm, fsec); + { + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '/'; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + } + *str++ = '/'; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); + *str++ = ' '; + str = pg_ltostr_zeropad(str, tm->tm_hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, tm->tm_min, 2); + *str++ = ':'; + str = AppendTimestampSeconds(str, tm, fsec); /* * Note: the uses of %.*s in this function would be risky if the * timezone names ever contain non-ASCII characters. However, all - * TZ abbreviations in the Olson database are plain ASCII. + * TZ abbreviations in the IANA database are plain ASCII. */ - if (print_tz) { if (tzn) - sprintf(str + strlen(str), " %.*s", MAXTZLEN, tzn); + { + sprintf(str, " %.*s", MAXTZLEN, tzn); + str += strlen(str); + } else - EncodeTimezone(str, tz, style); + str = EncodeTimezone(str, tz, style); } - - if (tm->tm_year <= 0) - sprintf(str + strlen(str), " BC"); break; case USE_GERMAN_DATES: /* German variant on European style */ - - sprintf(str, "%02d.%02d", tm->tm_mday, tm->tm_mon); - - sprintf(str + 5, ".%04d %02d:%02d:", - (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), - tm->tm_hour, tm->tm_min); - - AppendTimestampSeconds(str + strlen(str), tm, fsec); + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = '.'; + str = pg_ltostr_zeropad(str, tm->tm_mon, 2); + *str++ = '.'; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); + *str++ = ' '; + str = pg_ltostr_zeropad(str, tm->tm_hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, tm->tm_min, 2); + *str++ = ':'; + str = AppendTimestampSeconds(str, tm, fsec); if (print_tz) { if (tzn) - sprintf(str + strlen(str), " %.*s", MAXTZLEN, tzn); + { + sprintf(str, " %.*s", MAXTZLEN, tzn); + str += strlen(str); + } else - EncodeTimezone(str, tz, style); + str = EncodeTimezone(str, tz, style); } - - if (tm->tm_year <= 0) - sprintf(str + strlen(str), " BC"); break; case USE_POSTGRES_DATES: default: /* Backward-compatible with traditional Postgres abstime dates */ - day = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday); tm->tm_wday = j2day(day); - memcpy(str, days[tm->tm_wday], 3); - strcpy(str + 3, " "); - + str += 3; + *str++ = ' '; if (DateOrder == DATEORDER_DMY) - sprintf(str + 4, "%02d %3s", tm->tm_mday, months[tm->tm_mon - 1]); + { + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + *str++ = ' '; + memcpy(str, months[tm->tm_mon - 1], 3); + str += 3; + } else - sprintf(str + 4, "%3s %02d", months[tm->tm_mon - 1], tm->tm_mday); - - sprintf(str + 10, " %02d:%02d:", tm->tm_hour, tm->tm_min); - - AppendTimestampSeconds(str + strlen(str), tm, fsec); - - sprintf(str + strlen(str), " %04d", - (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1)); + { + memcpy(str, months[tm->tm_mon - 1], 3); + str += 3; + *str++ = ' '; + str = pg_ltostr_zeropad(str, tm->tm_mday, 2); + } + *str++ = ' '; + str = pg_ltostr_zeropad(str, tm->tm_hour, 2); + *str++ = ':'; + str = pg_ltostr_zeropad(str, tm->tm_min, 2); + *str++ = ':'; + str = AppendTimestampSeconds(str, tm, fsec); + *str++ = ' '; + str = pg_ltostr_zeropad(str, + (tm->tm_year > 0) ? tm->tm_year : -(tm->tm_year - 1), 4); if (print_tz) { if (tzn) - sprintf(str + strlen(str), " %.*s", MAXTZLEN, tzn); + { + sprintf(str, " %.*s", MAXTZLEN, tzn); + str += strlen(str); + } else { /* @@ -4079,15 +4206,19 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, bool print_tz, int tz, const char * avoid formatting something which would be rejected by * the date/time parser later. - thomas 2001-10-19 */ - sprintf(str + strlen(str), " "); - EncodeTimezone(str, tz, style); + *str++ = ' '; + str = EncodeTimezone(str, tz, style); } } - - if (tm->tm_year <= 0) - sprintf(str + strlen(str), " BC"); break; } + + if (tm->tm_year <= 0) + { + memcpy(str, " BC", 3); /* Don't copy NUL */ + str += 3; + } + *str = '\0'; } @@ -4242,7 +4373,8 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) day_sign, abs(mday), sec_sign, abs(hour), abs(min)); cp += strlen(cp); - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + *cp = '\0'; } else if (has_year_month) { @@ -4252,13 +4384,15 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) { sprintf(cp, "%d %d:%02d:", mday, hour, min); cp += strlen(cp); - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + *cp = '\0'; } else { sprintf(cp, "%d:%02d:", hour, min); cp += strlen(cp); - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + *cp = '\0'; } } break; @@ -4284,8 +4418,7 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) { if (sec < 0 || fsec < 0) *cp++ = '-'; - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, false); - cp += strlen(cp); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, false); *cp++ = 'S'; *cp++ = '\0'; } @@ -4311,7 +4444,8 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) (minus ? "-" : (is_before ? "+" : "")), abs(hour), abs(min)); cp += strlen(cp); - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, true); + *cp = '\0'; } break; @@ -4337,8 +4471,7 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) } else if (is_before) *cp++ = '-'; - AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, false); - cp += strlen(cp); + cp = AppendSeconds(cp, sec, fsec, MAX_INTERVAL_PRECISION, false); sprintf(cp, " sec%s", (abs(sec) != 1 || fsec != 0) ? "s" : ""); is_zero = FALSE; diff --git a/src/backend/utils/adt/numutils.c b/src/backend/utils/adt/numutils.c index 880d304fb5..6b105964bd 100644 --- a/src/backend/utils/adt/numutils.c +++ b/src/backend/utils/adt/numutils.c @@ -227,3 +227,164 @@ pg_lltoa(int64 value, char *a) *a-- = swap; } } + + +/* + * pg_ltostr_zeropad + * Converts 'value' into a decimal string representation stored at 'str'. + * 'minwidth' specifies the minimum width of the result; any extra space + * is filled up by prefixing the number with zeros. + * + * Returns the ending address of the string result (the last character written + * plus 1). Note that no NUL terminator is written. + * + * The intended use-case for this function is to build strings that contain + * multiple individual numbers, for example: + * + * str = pg_ltostr_zeropad(str, hours, 2); + * *str++ = ':'; + * str = pg_ltostr_zeropad(str, mins, 2); + * *str++ = ':'; + * str = pg_ltostr_zeropad(str, secs, 2); + * *str = '\0'; + * + * Note: Caller must ensure that 'str' points to enough memory to hold the + * result. + */ +char * +pg_ltostr_zeropad(char *str, int32 value, int32 minwidth) +{ + char *start = str; + char *end = &str[minwidth]; + int32 num = value; + + Assert(minwidth > 0); + + /* + * Handle negative numbers in a special way. We can't just write a '-' + * prefix and reverse the sign as that would overflow for INT32_MIN. + */ + if (num < 0) + { + *start++ = '-'; + minwidth--; + + /* + * Build the number starting at the last digit. Here remainder will + * be a negative number, so we must reverse the sign before adding '0' + * in order to get the correct ASCII digit. + */ + while (minwidth--) + { + int32 oldval = num; + int32 remainder; + + num /= 10; + remainder = oldval - num * 10; + start[minwidth] = '0' - remainder; + } + } + else + { + /* Build the number starting at the last digit */ + while (minwidth--) + { + int32 oldval = num; + int32 remainder; + + num /= 10; + remainder = oldval - num * 10; + start[minwidth] = '0' + remainder; + } + } + + /* + * If minwidth was not high enough to fit the number then num won't have + * been divided down to zero. We punt the problem to pg_ltostr(), which + * will generate a correct answer in the minimum valid width. + */ + if (num != 0) + return pg_ltostr(str, value); + + /* Otherwise, return last output character + 1 */ + return end; +} + +/* + * pg_ltostr + * Converts 'value' into a decimal string representation stored at 'str'. + * + * Returns the ending address of the string result (the last character written + * plus 1). Note that no NUL terminator is written. + * + * The intended use-case for this function is to build strings that contain + * multiple individual numbers, for example: + * + * str = pg_ltostr(str, a); + * *str++ = ' '; + * str = pg_ltostr(str, b); + * *str = '\0'; + * + * Note: Caller must ensure that 'str' points to enough memory to hold the + * result. + */ +char * +pg_ltostr(char *str, int32 value) +{ + char *start; + char *end; + + /* + * Handle negative numbers in a special way. We can't just write a '-' + * prefix and reverse the sign as that would overflow for INT32_MIN. + */ + if (value < 0) + { + *str++ = '-'; + + /* Mark the position we must reverse the string from. */ + start = str; + + /* Compute the result string backwards. */ + do + { + int32 oldval = value; + int32 remainder; + + value /= 10; + remainder = oldval - value * 10; + /* As above, we expect remainder to be negative. */ + *str++ = '0' - remainder; + } while (value != 0); + } + else + { + /* Mark the position we must reverse the string from. */ + start = str; + + /* Compute the result string backwards. */ + do + { + int32 oldval = value; + int32 remainder; + + value /= 10; + remainder = oldval - value * 10; + *str++ = '0' + remainder; + } while (value != 0); + } + + /* Remember the end+1 and back up 'str' to the last character. */ + end = str--; + + /* Reverse string. */ + while (start < str) + { + char swap = *start; + + *start++ = *str; + *str-- = swap; + } + + return end; +} diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index c9be32e33a..affcc01a40 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -290,6 +290,8 @@ extern int32 pg_atoi(const char *s, int size, int c); extern void pg_itoa(int16 i, char *a); extern void pg_ltoa(int32 l, char *a); extern void pg_lltoa(int64 ll, char *a); +extern char *pg_ltostr_zeropad(char *str, int32 value, int32 minwidth); +extern char *pg_ltostr(char *str, int32 value); /* * Per-opclass comparison functions for new btrees. These are