Table of Contents

Parsing Strings to Floats in C Without Locale Issues

Handling floating-point numbers from configuration files and user input is a common requirement in C programming. But when dealing with internationalization (i18n), locale settings can dramatically impact the way numbers are parsed—leading to subtle, hard-to-debug errors. Let’s explore the root of this problem, examine clear code samples, and discover robust ways to reliably convert strings to float in C, regardless of locale.


Understanding the Problem

Imagine you have a program that:

  • Reads numbers from configuration files, where decimals are always represented with a dot (.), like 3.14;
  • Accepts user input, which should respect the user’s locale (dot or comma as decimal separator);
  • Runs on systems around the world, each with their own locale preferences.

The issue? Some built-in C functions, like strtof() and strtod(), rely on the current process locale to interpret decimal points. For instance:

  • In the US locale, 3.14 parses as three and fourteen hundredths.
  • In some European locales (like fr_FR or de_DE), a decimal comma is expected: 3,14.
  • So, 3.14 will fail or be misinterpreted, possibly resulting in errors or the wrong values.

Example: Where Things Go Wrong

Suppose your application is running with the French locale, and you try to parse a configuration value:

#include <stdlib.h>
#include <stdio.h>
#include <locale.h>

int main() {
    setlocale(LC_ALL, "fr_FR.UTF-8"); // Set French locale
    const char* cfg_value = "0.123";   // Config always uses dot
    float val = strtof(cfg_value, NULL); // Parses with COMMA as decimal!
    printf("Value: %f\n", val); // Output might be just 0!
    return 0;
}

If the config string had a comma, it would work, but your config files are written with dots! This difference leads to bugs that show up only for some users, and only on some machines.


Why Not Just Change the Locale?

You might think: “Can’t I just change the program’s locale to C temporarily?” You could try:

setlocale(LC_ALL, "C");

But there are serious drawbacks:

  • Changing the process locale affects all threads—other parts of your program might break, especially in multi-threaded applications.
  • You lose proper i18n for user input.

Clearly, we need a thread-local, locale-independent way to parse configuration numbers.


Solution 1: Locale-Independent Parsing on POSIX

Use strtod_l() and strtof_l()

On POSIX systems (Linux, BSD, macOS), you have access to locale-aware variants of parsing functions that take a locale_t parameter. These let you specify the locale just for the current function call.

Example: Using the “C” Locale Only for Parsing

#include <stdlib.h>
#include <stdio.h>
#include <locale.h>

int main() {
    const char* cfg_value = "0.123"; // Config uses dot
    locale_t c_locale = newlocale(LC_ALL_MASK, "C", NULL);
    float val = strtof_l(cfg_value, NULL, c_locale); // Always dot!
    printf("Parsed value: %f\n", val);
    freelocale(c_locale);
    return 0;
}
  • The config string is always parsed with the dot as a decimal separator, no matter the global locale.
  • User input elsewhere in your program can continue to use regional settings.

Thread-Local Locale Swap (Advanced)

If your code must call functions that only respect the locale of the current thread (but not a specific locale object), you can use uselocale() to temporarily set a thread-local locale:

locale_t old = uselocale(c_locale); // Switch to 'C' locale for this thread
float val = strtof(cfg_value, NULL);
uselocale(old); // Restore original locale

This approach is safe for multi-threaded programs, because each thread controls its own locale setting. Just remember to create and destroy locale objects properly.

Note: strtof_l() and related functions are POSIX-specific, not standard C99.


Solution 2: Manual Substitution (Portable Fallback)

Suppose you’re working with a super-stripped-down C runtime that lacks these locale variants. In that case, you’ll need to ensure the string matches the current locale, possibly by substituting the decimal point manually after querying the current locale separator.

Step-by-step:

  1. Retrieve the current decimal separator with localeconv().
  2. Copy the input string, replacing '.' with the correct separator.
  3. Call strtof() as usual.

Example: Swapping Decimal Point Manually

#include <stdlib.h>
#include <stdio.h>
#include <locale.h>
#include <string.h>

float parse_config_float(const char* input) {
    struct lconv* lc = localeconv();
    char buf;
    strncpy(buf, input, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';

    // Replace dot with locale-specific decimal point
    for (char* p = buf; *p; ++p) {
        if (*p == '.') *p = lc->decimal_point;
    }

    return strtof(buf, NULL);
}

int main() {
    setlocale(LC_ALL, "fr_FR.UTF-8"); // For demo
    float val = parse_config_float("3.14");
    printf("Locale-aware value: %f\n", val); // Correctly parses as 3.14
    return 0;
}

Caveats:

  • This only works if the decimal separator is a single character.
  • Multibyte separators (rare, but possible) require more careful handling.

Solution 3: Using Safer Standard Library APIs (strtof_s)

For C11 with Annex K support, there’s a safer version: strtof_s. It’s not universally available, but when it is, it provides error-checking and makes it harder to get buffer overruns or unchecked failures.

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdlib.h>
#include <stdio.h>

int main() {
    const char *str = "987.654";
    char *endptr = NULL;
    float value;
    errno_t result = strtof_s(str, &endptr, &value);
    if (result != 0) {
        printf("Conversion error occurred\n");
        return 1;
    }
    printf("Safely converted value: %f\n", value);
    return 0;
}

But: This function still respects the current locale. If your configuration files always use dots, it’s best to combine the above approaches where possible.


Solution 4: Use a Locale-Independent Fast Parser (Third-Party)

If performance is critical and you’re open to external code, consider using libraries like fast_double_parser. They offer locale-independent parsing of floating-point values, typically using the C format (dot as decimal separator) regardless of system locale.


Alternate Quick Fixes

  • For very simple cases (no exponents, no special float values):
  • Write your own parser that parses the integer and fractional parts.
  • Only recommended if you control all input and need minimum dependency footprint.
  • If you only target English-like locales:
  • You might be able to call setlocale(LC_NUMERIC, "C"); just for the float parsing section, but beware of multi-threading issues.
  • Always validate and test on multiple locales:
  • Especially prior to release, running test cases under different LC_ALL settings catches many subtle bugs.

Summary Table: Methods Overview

| Method | Works With Locale? | Thread Safe? | Platform | Brief Notes |
|——————————- |——————- |—————|————-|————————————||
| strtof()/strtod() | Yes | No | Standard C | Locale-dependent |
| strtof_l()/strtod_l() | No (You choose!) | Yes | POSIX, BSD | Pass locale_t for parsing |
| uselocale() on thread | No (Selectable) | Yes | POSIX | Temporarily change thread locale |
| Manual substitution/composition | No (Works) | Yes | Any | Replace decimal point manually |
| strtof_s() | Yes | Safer | C11 Annex K | Bound-checked, but locale-bound |
| External parser | No | Yes | Depends | Libraries like fast_double_parser |


Final Recommendation

  • Read configuration files with a locale-independent parser. On Linux/BSD, use strtof_l or strtod_l with the “C” locale for parsing configuration values that always use dot as the decimal separator.
  • Parse user input with the current locale, so users can enter numbers the way they expect.
  • Avoid changing the global locale in threaded programs.

Sample: Robust Parsing Function for Config Files

#include <stdlib.h>
#include <stdio.h>
#include <locale.h>

float parse_config_float(const char* input) {
    #ifdef __APPLE__
    // macOS uses locale_t directly
    #else
    locale_t c_locale = newlocale(LC_ALL_MASK, "C", NULL);
    float val = strtof_l(input, NULL, c_locale);
    freelocale(c_locale);
    return val;
    #endif
}

By following these methods, you can write C code that safely parses floating-point numbers from configuration files, unaffected by user or system locale—while preserving full i18n support for user-facing features.