Proven C Book한국어 GitHub

69 Locales ② — numbers, money, time and sorting

What to know first

chapter 68, Locales ① · categories and setlocale
chapter 63, Streams in practice · printf formats

Looking 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

If chapter 68 was “what a locale is”, this is “so what changes”. Only in that order do the twenty-four members of 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

What a locale actually changes, one at a time. All twenty-four members of 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

  1. Then which standard function prints an amount?
  2. Is strcoll enough 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.

MemberWhat“C” locale
decimal_pointDecimal point, plain numbers"."
thousands_sepGroup separator, plain numbers""
groupingGrouping rule, plain numbers""
mon_decimal_pointDecimal point, money""
mon_thousands_sepGroup separator, money""
mon_groupingGrouping rule, money""
positive_signThe string for a non-negative amount""
negative_signThe string for a negative amount""
currency_symbolThe local currency symbol""
frac_digitsDigits after the decimal pointCHAR_MAX
p_cs_precedes, n_cs_precedesSymbol before the value? (1/0)CHAR_MAX
p_sep_by_space, n_sep_by_spaceA space between symbol and value (0/1/2)CHAR_MAX
p_sign_posn, n_sign_posnWhere the sign goes (0~4)CHAR_MAX
int_curr_symbolInternational symbol + one separator character""
int_frac_digits and six more int_*The same items for the international formCHAR_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 valueMeaning
CHAR_MAXNo further grouping is to be performed
0Repeat the previous element for the remaining digits
Anything elseThe 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_posnMeaning
0Parentheses surround the quantity and the currency symbol
1The sign string precedes the quantity and the symbol
2The sign string follows the quantity and the symbol
3The sign string immediately precedes the currency symbol
4The 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_precedesp_sign_posnp_sep_by_space=0p_sep_by_space=1
00(1.25$)(1.25 $)
01+1.25$+1.25 $
031.25+$1.25 +$
10($1.25)($ 1.25)
11+$1.25+$ 1.25
14$+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 as 3.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 ., and printf writes ,.
  • 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.

GroupSpecifiersNature
Locale decides%c %x %X %A %a %B %b %p %rDiffers by country — only for showing people
Locale-independent%Y %m %d %H %M %S %j %F %TThe 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_COLLATEstrcmp 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 𝑛log𝑛 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.

SystemMeansShape
POSIXThread-local localenewlocale/uselocale/freelocale
POSIXFunctions taking a localestrtod_l, strcoll_l, strftime_l, …
WindowsPer-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 wantHow
Dates, sorting and money in the user’s own waysetlocale(LC_ALL, "")
Writing numbers into files and protocolsPin LC_NUMERIC to "C"
Timestamps in logsstrftime with %F %T (ISO 8601)
Sorting a list for peoplestrcoll (or strxfrm keys)
Strings a machine comparesstrcmp — it must not be shaken by the locale
A different locale per threaduselocale / the _l family (outside the standard)
Writing a libraryDo not call setlocale

Table 70.7

Recap

What to rememberThe point
The windowlocaleconv alone. Do not modify it, do not keep it
struct lconvTwenty-four members. "" and CHAR_MAX mean “not specified”
int_curr_symbolThe first three characters are an ISO 4217 code
groupingAn array of numbers. CHAR_MAX=stop, 0=repeat. Not always three
MoneyAssembled 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 zoneTZ, not the locale
LC_COLLATEstrcmp ≠ dictionary order. Repeated comparison → strxfrm keys
ThreadsNothing 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.