Skip to content

Commit c2d6ef1

Browse files
committed
Fix psql's code for locale-aware formatting of numeric output.
This code did the wrong thing entirely for numbers with an exponent but no decimal point (e.g., '1e6'), as reported by Jeff Janes in bug #13636. More generally, it made lots of unverified assumptions about what the input string could possibly look like. Rearrange so that it only fools with leading digits that it's directly verified are there, and an immediately adjacent decimal point. While at it, get rid of some useless inefficiencies, like converting the grouping count string to integer over and over (and over). This has been broken for a long time, so back-patch to all supported branches.
1 parent 078d471 commit c2d6ef1

File tree

1 file changed

+45
-58
lines changed

1 file changed

+45
-58
lines changed

src/bin/psql/print.c

Lines changed: 45 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@
4040
*/
4141
volatile bool cancel_pressed = false;
4242

43+
/* info for locale-aware numeric formatting; set up by setDecimalLocale() */
4344
static char *decimal_point;
44-
static char *grouping;
45+
static int groupdigits;
4546
static char *thousands_sep;
4647

4748
/* Line style control structures */
@@ -154,100 +155,85 @@ pg_local_calloc(int count, size_t size)
154155
return tmp;
155156
}
156157

158+
/* Count number of digits in integral part of number */
157159
static int
158160
integer_digits(const char *my_str)
159161
{
160-
int frac_len;
161-
162-
if (my_str[0] == '-')
162+
/* ignoring any sign ... */
163+
if (my_str[0] == '-' || my_str[0] == '+')
163164
my_str++;
164-
165-
frac_len = strchr(my_str, '.') ? strlen(strchr(my_str, '.')) : 0;
166-
167-
return strlen(my_str) - frac_len;
165+
/* ... count initial integral digits */
166+
return strspn(my_str, "0123456789");
168167
}
169168

170-
/* Return additional length required for locale-aware numeric output */
169+
/* Compute additional length required for locale-aware numeric output */
171170
static int
172171
additional_numeric_locale_len(const char *my_str)
173172
{
174173
int int_len = integer_digits(my_str),
175174
len = 0;
176-
int groupdigits = atoi(grouping);
177175

178-
if (int_len > 0)
179-
/* Don't count a leading separator */
180-
len = (int_len / groupdigits - (int_len % groupdigits == 0)) *
181-
strlen(thousands_sep);
176+
/* Account for added thousands_sep instances */
177+
if (int_len > groupdigits)
178+
len += ((int_len - 1) / groupdigits) * strlen(thousands_sep);
182179

180+
/* Account for possible additional length of decimal_point */
183181
if (strchr(my_str, '.') != NULL)
184-
len += strlen(decimal_point) - strlen(".");
182+
len += strlen(decimal_point) - 1;
185183

186184
return len;
187185
}
188186

189-
static int
190-
strlen_with_numeric_locale(const char *my_str)
191-
{
192-
return strlen(my_str) + additional_numeric_locale_len(my_str);
193-
}
194-
195187
/*
196188
* Returns the appropriately formatted string in a new allocated block,
197189
* caller must free
198190
*/
199191
static char *
200192
format_numeric_locale(const char *my_str)
201193
{
194+
int new_len = strlen(my_str) + additional_numeric_locale_len(my_str);
195+
char *new_str = pg_local_malloc(new_len + 1);
196+
int int_len = integer_digits(my_str);
202197
int i,
203-
j,
204-
int_len = integer_digits(my_str),
205198
leading_digits;
206-
int groupdigits = atoi(grouping);
207-
int new_str_start = 0;
208-
char *new_str = new_str = pg_local_malloc(
209-
strlen_with_numeric_locale(my_str) + 1);
199+
int new_str_pos = 0;
210200

211-
leading_digits = (int_len % groupdigits != 0) ?
212-
int_len % groupdigits : groupdigits;
201+
/* number of digits in first thousands group */
202+
leading_digits = int_len % groupdigits;
203+
if (leading_digits == 0)
204+
leading_digits = groupdigits;
213205

214-
if (my_str[0] == '-') /* skip over sign, affects grouping
215-
* calculations */
206+
/* process sign */
207+
if (my_str[0] == '-' || my_str[0] == '+')
216208
{
217-
new_str[0] = my_str[0];
209+
new_str[new_str_pos++] = my_str[0];
218210
my_str++;
219-
new_str_start = 1;
220211
}
221212

222-
for (i = 0, j = new_str_start;; i++, j++)
213+
/* process integer part of number */
214+
for (i = 0; i < int_len; i++)
223215
{
224-
/* Hit decimal point? */
225-
if (my_str[i] == '.')
216+
/* Time to insert separator? */
217+
if (i > 0 && --leading_digits == 0)
226218
{
227-
strcpy(&new_str[j], decimal_point);
228-
j += strlen(decimal_point);
229-
/* add fractional part */
230-
strcpy(&new_str[j], &my_str[i] + 1);
231-
break;
232-
}
233-
234-
/* End of string? */
235-
if (my_str[i] == '\0')
236-
{
237-
new_str[j] = '\0';
238-
break;
239-
}
240-
241-
/* Add separator? */
242-
if (i != 0 && (i - leading_digits) % groupdigits == 0)
243-
{
244-
strcpy(&new_str[j], thousands_sep);
245-
j += strlen(thousands_sep);
219+
strcpy(&new_str[new_str_pos], thousands_sep);
220+
new_str_pos += strlen(thousands_sep);
221+
leading_digits = groupdigits;
246222
}
223+
new_str[new_str_pos++] = my_str[i];
224+
}
247225

248-
new_str[j] = my_str[i];
226+
/* handle decimal point if any */
227+
if (my_str[i] == '.')
228+
{
229+
strcpy(&new_str[new_str_pos], decimal_point);
230+
new_str_pos += strlen(decimal_point);
231+
i++;
249232
}
250233

234+
/* copy the rest (fractional digits and/or exponent, and \0 terminator) */
235+
strcpy(&new_str[new_str_pos], &my_str[i]);
236+
251237
return new_str;
252238
}
253239

@@ -2473,10 +2459,11 @@ setDecimalLocale(void)
24732459
decimal_point = pg_strdup(extlconv->decimal_point);
24742460
else
24752461
decimal_point = "."; /* SQL output standard */
2462+
24762463
if (*extlconv->grouping && atoi(extlconv->grouping) > 0)
2477-
grouping = pg_strdup(extlconv->grouping);
2464+
groupdigits = atoi(extlconv->grouping);
24782465
else
2479-
grouping = "3"; /* most common */
2466+
groupdigits = 3; /* most common */
24802467

24812468
/* similar code exists in formatting.c */
24822469
if (*extlconv->thousands_sep)

0 commit comments

Comments
 (0)