69 Locales ② — numbers, money, time and sorting
What to know first
setlocaleprintf formatsLooking back
Chapter 68 said a locale is data holding conventions. What exactly is that data made of, and how does a program get at it?
A. The window is surprisingly narrow — one function (localeconv) and one struct (struct lconv). Every convention about numbers and money sits in that struct’s twenty-four members.
For the rest there is no window at all. strftime writes dates by itself and strcoll compares by itself — the standard gives a program no way to look at that data. This asymmetry is the shape of the chapter: what you fetch (numbers and money) and what you delegate (time and sorting).
The need for this chapter, and its context
struct lconv become answers rather than a list. In particular the pattern where LC_NUMERIC corrupts data cannot have its cause named without the previous chapter’s notion of categories.By the end of this chapter
struct lconv, how the grouping string is encoded, the rule by which a monetary form is assembled, the pattern in which LC_NUMERIC corrupts data, strftime’s locale-dependent and locale-independent formats, strcoll and sort keys, and locales in threads.The questions this chapter answers
- Then which standard function prints an amount?
- Is
strcollenough for Korean sorting?
69.1 One window — localeconv
struct lconv *localeconv(void);What it returns is the address of a struct filled with the numeric and monetary conventions of the current locale. Two rules attach. A program must not modify the contents, and a later localeconv or a setlocale with LC_ALL, LC_MONETARY or LC_NUMERIC may overwrite them. It is a value to read on the spot, not to hold on to.
examples-en/ch69/lconv.c
/* struct lconv — everything a locale knows about numbers and money. */
#include <limits.h>
#include <locale.h>
#include <stdio.h>
/* String members: show emptiness visibly */
static const char *str(const char *s) { return (s && *s) ? s : "(empty)"; }
/* char members: CHAR_MAX means "this locale does not say" */
static void show_char(const char *name, char v)
{
if (v == CHAR_MAX) printf(" %-20s CHAR_MAX (not specified)\n", name);
else printf(" %-20s %d\n", name, v);
}
/* How the grouping string is encoded:
each byte is the group size at that position. CHAR_MAX means "no more
grouping", 0 means "repeat the previous value forever". "\3" is groups of
three without end; en_IN uses "\3\2". */
static void show_grouping(const char *name, const char *g)
{
printf("%-20s ", name);
if (!*g) { puts("(empty — no grouping)"); return; }
for (const char *p = g; *p; p++) {
if (*p == CHAR_MAX) printf("CHAR_MAX ");
else printf("%d ", *p);
}
puts("");
}
static void dump_all(void)
{
struct lconv *L = localeconv();
puts("[plain numbers]");
printf(" %-20s \"%s\"\n", "decimal_point", str(L->decimal_point));
printf(" %-20s \"%s\"\n", "thousands_sep", str(L->thousands_sep));
show_grouping(" grouping", L->grouping);
puts("[monetary numbers]");
printf(" %-20s \"%s\"\n", "mon_decimal_point", str(L->mon_decimal_point));
printf(" %-20s \"%s\"\n", "mon_thousands_sep", str(L->mon_thousands_sep));
show_grouping(" mon_grouping", L->mon_grouping);
printf(" %-20s \"%s\"\n", "positive_sign", str(L->positive_sign));
printf(" %-20s \"%s\"\n", "negative_sign", str(L->negative_sign));
printf(" %-20s \"%s\"\n", "currency_symbol", str(L->currency_symbol));
printf(" %-20s \"%s\"\n", "int_curr_symbol", str(L->int_curr_symbol));
show_char("frac_digits", L->frac_digits);
show_char("int_frac_digits", L->int_frac_digits);
puts("[how the monetary form is assembled]");
show_char("p_cs_precedes", L->p_cs_precedes);
show_char("p_sep_by_space", L->p_sep_by_space);
show_char("p_sign_posn", L->p_sign_posn);
show_char("n_cs_precedes", L->n_cs_precedes);
show_char("n_sep_by_space", L->n_sep_by_space);
show_char("n_sign_posn", L->n_sign_posn);
}
/* 여러 로케일을 한 줄씩 견주어 본다 */
static void compare_row(const char *name)
{
if (!setlocale(LC_ALL, name)) {
printf(" %-14s (not on this machine)\n", name);
return;
}
struct lconv *L = localeconv();
printf(" %-14s point \"%s\" thousands \"%s\" symbol \"%s\" ISO \"%s\"\n",
name, str(L->decimal_point), str(L->thousands_sep),
str(L->currency_symbol), str(L->int_curr_symbol));
}
int main(void)
{
/* A program starts in "C". The standard fixes these values (§7.11). */
puts("=== the \"C\" locale — the values the standard fixes ===");
dump_all();
puts("\n=== compared across locales ===");
static const char *names[] = {
"C", "en_US.UTF-8", "ko_KR.UTF-8", "de_DE.UTF-8",
"fr_FR.UTF-8", "ja_JP.UTF-8", "en_IN.UTF-8",
};
for (size_t i = 0; i < sizeof names / sizeof *names; i++)
compare_row(names[i]);
/* Grouping is not always three digits — the Indian system */
puts("\n=== grouping is not always three digits ===");
static const char *grp[] = { "C", "en_US.UTF-8", "en_IN.UTF-8" };
for (size_t i = 0; i < sizeof grp / sizeof *grp; i++) {
if (!setlocale(LC_ALL, grp[i])) {
printf(" %-14s (not on this machine)\n", grp[i]);
continue;
}
printf(" %-14s ", grp[i]);
show_grouping("grouping", localeconv()->grouping);
}
puts(" -> en_IN's 3,2 means \"three from the right, then two at a time\" —");
puts(" 12345678 is grouped as 1,23,45,678 (the lakh/crore system).");
/* Change the decimal point and printf and strtod change together */
puts("\n=== LC_NUMERIC changes printf ===");
static const char *num_locales[] = { "C", "de_DE.UTF-8", "fr_FR.UTF-8" };
for (size_t i = 0; i < sizeof num_locales / sizeof *num_locales; i++) {
if (!setlocale(LC_NUMERIC, num_locales[i])) {
printf(" %-14s (not on this machine)\n", num_locales[i]);
continue;
}
printf(" %-14s printf(\"%%.2f\", 1234.5) -> %.2f\n",
num_locales[i], 1234.5);
}
return 0;
}
Output
=== the "C" locale — the values the standard fixes ===
[plain numbers]
decimal_point "."
thousands_sep "(empty)"
grouping (empty — no grouping)
[monetary numbers]
mon_decimal_point "(empty)"
mon_thousands_sep "(empty)"
mon_grouping (empty — no grouping)
positive_sign "(empty)"
negative_sign "(empty)"
currency_symbol "(empty)"
int_curr_symbol "(empty)"
frac_digits CHAR_MAX (not specified)
int_frac_digits CHAR_MAX (not specified)
[how the monetary form is assembled]
p_cs_precedes CHAR_MAX (not specified)
p_sep_by_space CHAR_MAX (not specified)
p_sign_posn CHAR_MAX (not specified)
n_cs_precedes CHAR_MAX (not specified)
n_sep_by_space CHAR_MAX (not specified)
n_sign_posn CHAR_MAX (not specified)
=== compared across locales ===
C point "." thousands "(empty)" symbol "(empty)" ISO "(empty)"
en_US.UTF-8 point "." thousands "," symbol "$" ISO "USD "
ko_KR.UTF-8 point "." thousands "," symbol "₩" ISO "KRW "
de_DE.UTF-8 point "," thousands "." symbol "€" ISO "EUR "
fr_FR.UTF-8 point "," thousands " " symbol "€" ISO "EUR "
ja_JP.UTF-8 point "." thousands "," symbol "¥" ISO "JPY "
en_IN.UTF-8 point "." thousands "," symbol "₹" ISO "INR "
=== grouping is not always three digits ===
C grouping (empty — no grouping)
en_US.UTF-8 grouping 3
en_IN.UTF-8 grouping 3 2
-> en_IN's 3,2 means "three from the right, then two at a time" —
12345678 is grouped as 1,23,45,678 (the lakh/crore system).
=== LC_NUMERIC changes printf ===
C printf("%.2f", 1234.5) -> 1234.50
de_DE.UTF-8 printf("%.2f", 1234.5) -> 1234,50
fr_FR.UTF-8 printf("%.2f", 1234.5) -> 1234,50
69.1.1 The twenty-four members at a glance
The standard says the struct shall contain at least the following members, in any order. So reach them by name, never by position or designated order.
| Member | What | “C” locale |
|---|---|---|
decimal_point | Decimal point, plain numbers | "." |
thousands_sep | Group separator, plain numbers | "" |
grouping | Grouping rule, plain numbers | "" |
mon_decimal_point | Decimal point, money | "" |
mon_thousands_sep | Group separator, money | "" |
mon_grouping | Grouping rule, money | "" |
positive_sign | The string for a non-negative amount | "" |
negative_sign | The string for a negative amount | "" |
currency_symbol | The local currency symbol | "" |
frac_digits | Digits after the decimal point | CHAR_MAX |
p_cs_precedes, n_cs_precedes | Symbol before the value? (1/0) | CHAR_MAX |
p_sep_by_space, n_sep_by_space | A space between symbol and value (0/1/2) | CHAR_MAX |
p_sign_posn, n_sign_posn | Where the sign goes (0~4) | CHAR_MAX |
int_curr_symbol | International symbol + one separator character | "" |
int_frac_digits and six more int_* | The same items for the international form | CHAR_MAX |
Table 70.1
Two conventions must be read. A string member of "" means this locale does not specify that value (decimal_point alone always has one), and a char member of CHAR_MAX means the same. The CHAR_MAX values the demonstration printed in the "C" locale are exactly that — the C locale says nothing at all about money.
For int_curr_symbol the standard reaches straight into another international standard. The first three characters are the alphabetic international currency symbol of ISO 4217, and the fourth is the character separating that symbol from the amount. That is why "KRW " and "EUR " came out with a trailing space.
69.1.2 How the grouping string is encoded
grouping is not a string for people to read; it is an array of numbers. The standard fixes the reading.
| Element value | Meaning |
|---|---|
CHAR_MAX | No further grouping is to be performed |
0 | Repeat the previous element for the remaining digits |
| Anything else | The size of the group at this position; the next element sizes the group before it |
Table 70.2
So "\3" looks as though it should mean “one group of three and no more”, yet real locales produce 1,234,567 from it, because glibc treats the last element as repeating. To be explicit, write "\3\0".
What the demonstration measured is en_IN’s 3 2. Three digits from the right, then two at a time — 12345678 becomes 1,23,45,678. It is India’s lakh-crore system, and living proof that the assumption of fixed groups of three is wrong.
69.1.3 A monetary form is assembled from three values
Why does printing one amount need six members? Because the real forms differ that much. The combination of p_cs_precedes (symbol first?), p_sep_by_space (a space?) and p_sign_posn (where the sign goes) decides the form.
p_sign_posn | Meaning |
|---|---|
| 0 | Parentheses surround the quantity and the currency symbol |
| 1 | The sign string precedes the quantity and the symbol |
| 2 | The sign string follows the quantity and the symbol |
| 3 | The sign string immediately precedes the currency symbol |
| 4 | The sign string immediately follows the currency symbol |
Table 70.3
The standard carries the resulting table itself. With $ as the symbol and + as the sign, printing 1.25 splits like this (an excerpt).
p_cs_precedes | p_sign_posn | p_sep_by_space=0 | p_sep_by_space=1 |
|---|---|---|---|
| 0 | 0 | (1.25$) | (1.25 $) |
| 0 | 1 | +1.25$ | +1.25 $ |
| 0 | 3 | 1.25+$ | 1.25 +$ |
| 1 | 0 | ($1.25) | ($ 1.25) |
| 1 | 1 | +$1.25 | +$ 1.25 |
| 1 | 4 | $+1.25 | $+ 1.25 |
Table 70.4
That the accountant’s parentheses (a p_sign_posn of 0) are in the standard is worth noticing — the convention of writing a negative as (1,234) rather than -1,234.
Q. Then which standard function prints an amount?
A. There is none in standard C. localeconv hands over the materials; the assembly is the program’s job. You must look at those six members and build the string as the table above says.
POSIX has strfmon to do it for you (strfmon(buf, n, "%n", 1234.5)). Windows has GetCurrencyFormat. Neither is standard C, so where portability matters you assemble it yourself or use a library.
A more important discipline, in passing: do not hold money in a double (chapter 50). Keep it as an integer number of the smallest unit and insert the decimal point only when displaying.
69.2 LC_NUMERIC — where data is quietly corrupted
The most practical warning in this chapter. LC_NUMERIC changes not only what localeconv reports but the decimal-point character printf, scanf and strtod themselves use.
The last part of the demonstration is that. The same printf("%.2f", 1234.5) prints 1234.50 in one place and 1234,50 in another. And strtod("3.14", …) stops after 3 in a locale whose decimal point is a comma.
In practice. How one decimal point stopped a server
The same program writing 3.14 on the developer’s machine (English locale) and 3,14 on the user’s (German locale) has happened over and over.
- A configuration file cannot be read back — written as
3,14, expected as3.14. - A CSV loses its columns — the comma inside a value collides with the column separator.
- JSON between two servers disagrees — the JSON standard nails the decimal point to
., andprintfwrites,. - Numbers in the logs cannot be aggregated.
They have one thing in common. All of them sent a number a machine will read down the path meant for people. There is one prescription — pin LC_NUMERIC to "C" with chapter 68′s idiom, and format separately when showing a person.
A common misconception. “Our service is only used inside one country, so locales are not our problem”
It catches you in two places. First, the locale is settled by the user’s machine. The same program runs under a different locale on someone else’s computer — the desktop’s settings, or a container image’s environment variables.
Second, a Korean locale is not "C" either. ko_KR.UTF-8 happens to use . as its decimal point, but its grouping, dates and sorting all differ. Try to parse back a date printed with %c and it catches you there.
69.3 LC_TIME — writing dates and times
The time arithmetic itself has nothing to do with the locale — <time.h> gets its proper treatment in chapter 74; here we only borrow its format characters. What the locale changes is the writing, and the window is strftime’s conversion specifiers.
examples-en/ch69/time_locale.c
/* LC_TIME — one instant written differently. And the formats that never move. */
#include <locale.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
/* A fixed instant so the run is reproducible: 2026-08-06 (Thu) 15:04:05 */
static struct tm fixed_time(void)
{
struct tm t = {
.tm_year = 2026 - 1900, .tm_mon = 8 - 1, .tm_mday = 6,
.tm_hour = 15, .tm_min = 4, .tm_sec = 5,
.tm_wday = 4, /* Thursday */
.tm_yday = 217, .tm_isdst = 0,
};
return t;
}
static void row(const char *locale, const char *fmt)
{
char buf[256];
struct tm t = fixed_time();
if (!setlocale(LC_TIME, locale)) {
printf(" %-14s (not on this machine)\n", locale);
return;
}
size_t n = strftime(buf, sizeof buf, fmt, &t);
if (n == 0) { printf(" %-14s (buffer too small)\n", locale); return; }
printf(" %-14s %s\n", locale, buf);
}
int main(void)
{
static const char *locales[] = {
"C", "en_US.UTF-8", "ko_KR.UTF-8", "de_DE.UTF-8",
"fr_FR.UTF-8", "ja_JP.UTF-8",
};
static const struct { const char *fmt, *what; } cases[] = {
{ "%c", "%c — the locale's own date-and-time form" },
{ "%x", "%x — the locale's own date form" },
{ "%X", "%X — the locale's own time form" },
{ "%A %B", "%A %B — full names of the day and the month" },
{ "%a %b", "%a %b — abbreviated names" },
{ "%p %I:%M", "%p %I:%M — am/pm and the 12-hour clock" },
{ "%F %T", "%F %T — ISO 8601. *independent of the locale*" },
{ "%Y-%m-%d", "%Y-%m-%d — numeric forms are locale-independent too" },
};
for (size_t c = 0; c < sizeof cases / sizeof *cases; c++) {
printf("\n%s\n", cases[c].what);
for (size_t i = 0; i < sizeof locales / sizeof *locales; i++)
row(locales[i], cases[c].fmt);
}
/* The time zone comes from TZ, not from the locale — a common mix-up. */
puts("\nThe zone is set by TZ, not by LC_TIME:");
setlocale(LC_TIME, "C");
struct tm t = fixed_time();
char buf[128];
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S %Z(%z)", &t);
printf(" %s\n", buf);
return 0;
}
Output
%c — the locale's own date-and-time form
C Thu Aug 6 15:04:05 2026
en_US.UTF-8 Thu 06 Aug 2026 03:04:05 PM KST
ko_KR.UTF-8 2026년 08월 06일 (목) 오후 03시 04분 05초
de_DE.UTF-8 Do 06 Aug 2026 15:04:05 KST
fr_FR.UTF-8 jeu. 06 août 2026 15:04:05
ja_JP.UTF-8 2026年08月06日 15時04分05秒
%x — the locale's own date form
C 08/06/26
en_US.UTF-8 08/06/2026
ko_KR.UTF-8 2026년 08월 06일
de_DE.UTF-8 06.08.2026
fr_FR.UTF-8 06/08/2026
ja_JP.UTF-8 2026年08月06日
%X — the locale's own time form
C 15:04:05
en_US.UTF-8 03:04:05 PM
ko_KR.UTF-8 15시 04분 05초
de_DE.UTF-8 15:04:05
fr_FR.UTF-8 15:04:05
ja_JP.UTF-8 15時04分05秒
%A %B — full names of the day and the month
C Thursday August
en_US.UTF-8 Thursday August
ko_KR.UTF-8 목요일 8월
de_DE.UTF-8 Donnerstag August
fr_FR.UTF-8 jeudi août
ja_JP.UTF-8 木曜日 8月
%a %b — abbreviated names
C Thu Aug
en_US.UTF-8 Thu Aug
ko_KR.UTF-8 목 8월
de_DE.UTF-8 Do Aug
fr_FR.UTF-8 jeu. août
ja_JP.UTF-8 木 8月
%p %I:%M — am/pm and the 12-hour clock
C PM 03:04
en_US.UTF-8 PM 03:04
ko_KR.UTF-8 오후 03:04
de_DE.UTF-8 03:04
fr_FR.UTF-8 03:04
ja_JP.UTF-8 午後 03:04
%F %T — ISO 8601. *independent of the locale*
C 2026-08-06 15:04:05
en_US.UTF-8 2026-08-06 15:04:05
ko_KR.UTF-8 2026-08-06 15:04:05
de_DE.UTF-8 2026-08-06 15:04:05
fr_FR.UTF-8 2026-08-06 15:04:05
ja_JP.UTF-8 2026-08-06 15:04:05
%Y-%m-%d — numeric forms are locale-independent too
C 2026-08-06
en_US.UTF-8 2026-08-06
ko_KR.UTF-8 2026-08-06
de_DE.UTF-8 2026-08-06
fr_FR.UTF-8 2026-08-06
ja_JP.UTF-8 2026-08-06
The zone is set by TZ, not by LC_TIME:
2026-08-06 15:04:05 KST(+0000)
The demonstration prints one instant in six locales. The knack is to split the specifiers into two groups.
| Group | Specifiers | Nature |
|---|---|---|
| Locale decides | %c %x %X %A %a %B %b %p %r | Differs by country — only for showing people |
| Locale-independent | %Y %m %d %H %M %S %j %F %T | The same everywhere — for recording, sending, parsing |
Table 70.5
%F %T produces ISO 8601 (2026-08-06 15:04:05) exactly. Using only these in logs, file names and API responses is the discipline. In the demonstration these two are the only lines identical across all six locales.
Platform note. The time zone is not the locale
A common mix-up. What %Z (zone name) and %z (offset) show is settled by the TZ environment variable and tzset, not by LC_TIME. “I changed the locale to Korea and the time is still wrong” usually means TZ was not changed.
Locale and time zone are different axes — the locale says how to write it, the zone says when it is. Printing Seoul time with a German locale is a perfectly normal combination.
69.4 LC_COLLATE — strcmp is not dictionary order
strcmp compares byte values. So "Zebra" sorts before "apfel" (Z=0x5A < a=0x61), and Hangul lines up in code-point order. That is not what a reader expects.
examples-en/ch69/collate.c
/* LC_COLLATE — strcmp is not dictionary order. strcoll, and sort keys. */
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *words[] = {
"Zebra", "apfel", "Apfel", "Äpfel", "banane", "Öl", "Zoo", "zoo",
};
#define N (sizeof words / sizeof *words)
static int by_strcmp(const void *a, const void *b)
{ return strcmp(*(const char *const *)a, *(const char *const *)b); }
static int by_strcoll(const void *a, const void *b)
{ return strcoll(*(const char *const *)a, *(const char *const *)b); }
static void sort_and_show(const char *label, int (*cmp)(const void *, const void *))
{
const char *v[N];
memcpy(v, words, sizeof v);
qsort(v, N, sizeof *v, cmp);
printf(" %-10s", label);
for (size_t i = 0; i < N; i++) printf(" %s", v[i]);
putchar('\n');
}
int main(void)
{
static const char *locales[] = { "C", "en_US.UTF-8", "de_DE.UTF-8", "ko_KR.UTF-8" };
puts("The same words sorted two ways.");
printf(" input ");
for (size_t i = 0; i < N; i++) printf(" %s", words[i]);
puts("\n");
for (size_t i = 0; i < sizeof locales / sizeof *locales; i++) {
if (!setlocale(LC_ALL, locales[i])) {
printf("[%s] not on this machine\n\n", locales[i]);
continue;
}
printf("[%s]\n", locales[i]);
sort_and_show("strcmp", by_strcmp); /* byte order — locale-independent */
sort_and_show("strcoll", by_strcoll); /* the locale's dictionary order */
putchar('\n');
}
/* strxfrm — when comparing many times, build a "sort key" once.
Keys compared with strcmp come out in the same order as strcoll. */
puts("strxfrm freezes a locale comparison into a key:");
for (size_t i = 0; i < sizeof locales / sizeof *locales; i++) {
if (!setlocale(LC_ALL, locales[i])) continue;
char key[64];
size_t need = strxfrm(key, "Äpfel", sizeof key);
printf(" %-12s \"Äpfel\" -> key of %zu bytes: ", locales[i], need);
if (need >= sizeof key) { puts("(buffer too small)"); continue; }
for (size_t k = 0; k < need && k < 12; k++)
printf("%02X ", (unsigned char)key[k]);
puts(need > 12 ? "..." : "");
}
puts("\nDo the keys compare the same way strcoll does?");
for (size_t i = 0; i < sizeof locales / sizeof *locales; i++) {
if (!setlocale(LC_ALL, locales[i])) continue;
char ka[64], kb[64];
const char *a = "Äpfel", *b = "banane";
if (strxfrm(ka, a, sizeof ka) >= sizeof ka) continue;
if (strxfrm(kb, b, sizeof kb) >= sizeof kb) continue;
int c1 = strcoll(a, b), c2 = strcmp(ka, kb);
printf(" %-12s strcoll %s 0, key strcmp %s 0 — %s\n", locales[i],
c1 < 0 ? "<" : c1 > 0 ? ">" : "=",
c2 < 0 ? "<" : c2 > 0 ? ">" : "=",
((c1 < 0) == (c2 < 0) && (c1 > 0) == (c2 > 0)) ? "same" : "different");
}
return 0;
}
Output
The same words sorted two ways.
input Zebra apfel Apfel Äpfel banane Öl Zoo zoo
[C]
strcmp Apfel Zebra Zoo apfel banane zoo Äpfel Öl
strcoll Apfel Zebra Zoo apfel banane zoo Äpfel Öl
[en_US.UTF-8]
strcmp Apfel Zebra Zoo apfel banane zoo Äpfel Öl
strcoll apfel Apfel Äpfel banane Öl Zebra zoo Zoo
[de_DE.UTF-8]
strcmp Apfel Zebra Zoo apfel banane zoo Äpfel Öl
strcoll apfel Apfel Äpfel banane Öl Zebra zoo Zoo
[ko_KR.UTF-8]
strcmp Apfel Zebra Zoo apfel banane zoo Äpfel Öl
strcoll Öl Äpfel Apfel Zebra Zoo apfel banane zoo
strxfrm freezes a locale comparison into a key:
C "Äpfel" -> key of 6 bytes: C3 84 70 66 65 6C
en_US.UTF-8 "Äpfel" -> key of 43 bytes: 51 C4 9A C2 95 7E C3 90 01 02 0D 02 ...
de_DE.UTF-8 "Äpfel" -> key of 43 bytes: 51 C4 9A C2 95 7E C3 90 01 02 0D 02 ...
ko_KR.UTF-8 "Äpfel" -> key of 6 bytes: 03 03 6E 64 63 6A
Do the keys compare the same way strcoll does?
C strcoll > 0, key strcmp > 0 — same
en_US.UTF-8 strcoll < 0, key strcmp < 0 — same
de_DE.UTF-8 strcoll < 0, key strcmp < 0 — same
ko_KR.UTF-8 strcoll < 0, key strcmp < 0 — same
The result shows the difference plainly. In the "C" locale strcoll and strcmp give the same answer, but elsewhere they part — in German, Äpfel comes right after apfel, and case is interleaved rather than separated.
69.4.1 strxfrm — why such a function exists
strxfrm turns a string into a sort key. Keys compared with an ordinary strcmp come out in the same order as strcoll. The last part of the demonstration confirms it.
Why is it needed? One strcoll is not cheap — the locale’s rules must be applied every time. Sorting n items takes roughly comparisons, so it is better to transform n times and compare cheaply.
/* build the keys before sorting */
size_t need = strxfrm(NULL, s, 0); /* ask for the size first */
char *key = malloc(need + 1); /* take that much */
strxfrm(key, s, need + 1); /* and fill it */The key sizes the demonstration printed reveal the function’s character. In the "C" locale the key is the same 6 bytes as the original; in the German locale it is 43 — locale collation stacks several levels of weight (base letter, then accent, then case).
Q. Is strcoll enough for Korean sorting?
A. For a simple list, yes. Hangul syllables are already arranged in dictionary order in Unicode, so ko_KR.UTF-8′s strcoll gives the expected order.
Real-world sorting adds rules, though — natural number order (“file2” before “file10”), grouping by initial consonant, folding Chinese characters by their reading, ignoring case and spaces. That is beyond strcoll and belongs to Unicode’s collation algorithm (UTS #10) and its implementation, ICU.
Draw the line like this — strcoll for what locale collation covers, a dedicated library beyond it. Only avoid sorting a human-facing list with strcmp.
69.5 Locales and threads
Chapter 68 said the locale is process-global. In a program with several threads that becomes a problem — one thread changing the locale to print a date for a user shakes another thread’s printf("%f").
Standard C has no remedy. What exists are extensions.
| System | Means | Shape |
|---|---|---|
| POSIX | Thread-local locale | newlocale/uselocale/freelocale |
| POSIX | Functions taking a locale | strtod_l, strcoll_l, strftime_l, … |
| Windows | Per-thread locale mode | _configthreadlocale, _locale_t and _l functions |
Table 70.6
The _l family is the real remedy — it names the locale for that call only, touching no global state. Where portability matters you end up writing a thin layer over the two.
Counter-example. Calling setlocale inside a library
/* a library function */
double parse_number(const char *s) {
setlocale(LC_NUMERIC, "C"); /* changes someone else's program state */
return strtod(s, NULL);
}These three lines quietly wreck the application’s date and currency formatting. And between threads it is a data race.
A library has one discipline — read the locale, never change it. If you need parsing that the locale cannot shake, use strtod_l or handle the digits yourself.
69.6 Prescriptions
| What you want | How |
|---|---|
| Dates, sorting and money in the user’s own way | setlocale(LC_ALL, "") |
| Writing numbers into files and protocols | Pin LC_NUMERIC to "C" |
| Timestamps in logs | strftime with %F %T (ISO 8601) |
| Sorting a list for people | strcoll (or strxfrm keys) |
| Strings a machine compares | strcmp — it must not be shaken by the locale |
| A different locale per thread | uselocale / the _l family (outside the standard) |
| Writing a library | Do not call setlocale |
Table 70.7
Recap
| What to remember | The point |
|---|---|
| The window | localeconv alone. Do not modify it, do not keep it |
struct lconv | Twenty-four members. "" and CHAR_MAX mean “not specified” |
int_curr_symbol | The first three characters are an ISO 4217 code |
grouping | An array of numbers. CHAR_MAX=stop, 0=repeat. Not always three |
| Money | Assembled from cs_precedes, sep_by_space, sign_posn. No standard function |
LC_NUMERIC | ★ Changes the decimal point of printf and strtod — the chief corrupter |
LC_TIME | %c %x %X %A %B %p follow the locale; %F %T %Y-%m-%d do not |
| Time zone | TZ, not the locale |
LC_COLLATE | strcmp ≠ dictionary order. Repeated comparison → strxfrm keys |
| Threads | Nothing in the standard. uselocale, the _l family |
Table 70.8
We have followed what a locale changes to the end. One axis remains — the multibyte and wide characters governed by LC_CTYPE. The next chapter takes up what wchar_t really is and how a byte string unfolds into characters, one step at a time.