Improve speed of timestamp/time/date output functions.
authorTom Lane <tgl@sss.pgh.pa.us>
Sun, 7 Feb 2016 04:11:28 +0000 (23:11 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Sun, 7 Feb 2016 04:11:28 +0000 (23:11 -0500)
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
src/backend/utils/adt/numutils.c
src/include/utils/builtins.h

index 90b1eb16e4de165e12f0ee185cf3d626af27e256..cdbf72cf699de80412949e39ffe6d9509ddf3f93 100644 (file)
@@ -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);
-                       else
-                               sprintf(str, "%02d/%02d", tm->tm_mon, tm->tm_mday);
-                       if (tm->tm_year > 0)
-                               sprintf(str + 5, "/%04d", tm->tm_year);
+                       {
+                               str = pg_ltostr_zeropad(str, tm->tm_mday, 2);
+                               *str++ = '/';
+                               str = pg_ltostr_zeropad(str, tm->tm_mon, 2);
+                       }
                        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);
-                       else
-                               sprintf(str, "%02d-%02d", tm->tm_mon, tm->tm_mday);
-                       if (tm->tm_year > 0)
-                               sprintf(str + 5, "-%04d", tm->tm_year);
+                       {
+                               str = pg_ltostr_zeropad(str, tm->tm_mday, 2);
+                               *str++ = '-';
+                               str = pg_ltostr_zeropad(str, tm->tm_mon, 2);
+                       }
                        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;
index 880d304fb5289cdb0f8ad8d5ae2ea168647f63f2..6b105964bd1be1fde83df0eac2cb8f8bffaf03c6 100644 (file)
@@ -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;
+}
index c9be32e33acddfc560a3a471b8abee0f7815db00..affcc01a409e6dbbd47c4700378066f8fadd35a2 100644 (file)
@@ -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