为 C printf 设置千位分隔符

Set thousands separator for C printf

我有这个 C 代码:

locale_t myLocale = newlocale(LC_NUMERIC_MASK, "en_US", (locale_t) 0);
uselocale(myLocale);
ptrLocale = localeconv();
ptrLocale->thousands_sep = (char *) "'";

int i1 = snprintf( s1, sizeof(s1), "%'d", 123456789);

s1 中的输出是 123,456,789

即使我将 ->thousands_sep 设置为 ' 它也被忽略了。有没有办法将任何字符设置为千位分隔符?

函数 localeconv() 只是读取定位设置,ptrLocale->thousands_sep 本身不会更改当前语言环境的设置。

编辑:

我不知道如何在 C 中执行此操作,但可以找到许多 C++ 输出的示例。 请参阅以下 C++ 示例:

#include <iostream>
#include <locale>
using namespace std;

struct myseps : numpunct<char> { 
   // use ' as separator
   char do_thousands_sep() const { return '\''; } 

   // digits are grouped by 3
   string do_grouping() const { return ""; }
};

int main() {
  cout.imbue(locale(locale(), new myseps));
  cout << 1234567; // the result will be 1'234'567
}

编辑 2:

C++ 参考说:

localeconv() returns a pointer to a filled-in object of type struct lconv. The values contained in the object can be overwritten by subsequent calls to localeconv and do not directly modify the object. Calls to setlocale with category values of LC_ALL, LC_MONETARY, or LC_NUMERIC overwrite the contents of the structure.

我在 MS Visual Studio 2012 中尝试了以下示例(我知道这是糟糕且不安全的风格):

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

int main() {
    setlocale(LC_NUMERIC, "");
    struct lconv *ptrLocale = localeconv();
    strcpy(ptrLocale->decimal_point, ":");
    strcpy(ptrLocale->thousands_sep, "'");
    char str[20];
    printf("%10.3lf \n", 13000.26);
    return 0;
}

我看到了结果:

  13000:260

因此,可以假设 decimal_pointthousands_sep 的变化可以通过 localeconv() 接收到的指针来实现,但是 printf 忽略 thousands_sep .

编辑 3:

更新的 C++ 示例:

#include <iostream>
#include <locale>
#include <sstream>
using namespace std;

struct myseps : numpunct<char> { 
   // use ' as separator
   char do_thousands_sep() const { return '\''; } 

   // digits are grouped by 3
   string do_grouping() const { return ""; }
};

int main() {
  stringstream ss;
  ss.imbue(locale(locale(), new myseps));
  ss << 1234567;  // printing to string stream with formating
  printf("%s\n", ss.str().c_str()); // just output when ss.str() provide string, and c_str() converts it to char*
}

此答案来自

根据 this source,千位分隔符仅与非标准 ' 标志一起使用。

因此,如果您的 printf 与 POSIX.1-2008 兼容,您可以使用:

setlocale(LC_NUMERIC, "");
struct lconv *ptrLocale = localeconv();
ptrLocale->decimal_point = ":";
ptrLocale->thousands_sep = "'";
char str[20];
printf("%'10.3lf \n", 13000.26);
return 0;

如何更改 printf():

的千位分隔符是一个非常肮脏的技巧
  1. 下载 GNU libc。
  2. 运行 configure --prefix=/usr/glibc-version 命令
  3. 运行 make -j 8
  4. make 输出中获取带有所有开关的非常长的编译器命令
  5. 编写C源文件setMyThousandSeparator.c - 内容见下文
  6. 使用第 3 点的 gcc 开关编译此源文件。
  7. 在您的普通 C 源代码中,在 printf() 调用之前调用 setMyThousandSeparator("'") 函数。
  8. link setMyThousandSeparator.o 与您的项目。

目前我在 linking libc 静态时尝试了它,但它有效。

setMyThousandSeparator.c的内容:

#include <locale/localeinfo.h>

void setMyThousandSeparator(char * sMySeparator)
{
    _NL_CURRENT (LC_NUMERIC, THOUSANDS_SEP) = sMySeparator;
}

信息: 此解决方案是线程安全的,因为它访问的数据与 printf() 访问的数据相同!

这是一个非常简单的解决方案,适用于每个 linux 发行版,并且不需要 - 作为我的第一个答案 - glibc hack:


所有这些步骤必须在 origin glibc 目录中执行 - NOT 在构建目录中 - 在构建之后glibc 版本使用此 instructions.

建议的单独构建目录

我的新 locale 文件名为 en_AT

  1. 从现有文件 localedata/locales/ 目录中创建一个新文件 en_AT .
  2. thousands_sep 的所有条目更改为 thousands_sep "<U0027>" 或您希望用作千位分隔符的任何字符。
  3. 将新文件中所有出现的 en_US 更改为 en_AT
  4. 向文件 localedata/SUPPORTED 添加行:en_AT.UTF-8/UTF-8 \.
  5. 运行 在 build 目录 make localedata/install-locales.
  6. 新的 locale 将自动添加到系统中,立即 可供程序访问。

在 C/C++ 程序中,您切换到新的千位分隔符:

setlocale( LC_ALL, "en_AT.UTF-8" );

将它与 printf( "%'d", 1000000 ); 一起使用会产生此输出

1'000'000


备注:当您在程序中需要在运行时确定的不同本地化时,您可以在 man 页面中使用此 example加载请求的 locale 并替换 en_AT.

中的 LC_NUMERIC 设置

这是我用于 uint64_t 类型的专用 C 函数,但它可以很容易地泛化。基本上,它将千位分隔符注入 snprintf() 生成的字符串中。

此方法独立于 LOCALE、使用的 C 标准等 - 当然,您不必重新编译 GNU libc ;)

#if __WORDSIZE == 64
   #define PRT_U64 "lu"
#else
   #define PRT_U64 "llu"
#endif

char* th_sep_u64(uint64_t val, char* buf) {
   char tmpbuf[32]; //18'446'744'073'709'551'615 -> 26 chars
   int  nch, toffs, pos;
   pos   = 1;
   toffs = 31;
   nch   = snprintf(tmpbuf, 32, "%"PRT_U64, val);
   nch  -- ;
   buf[toffs] = 0;

   for (; nch>=0; --nch) {
      toffs -- ;
      buf[toffs] = tmpbuf[nch];
      if ((0 == (pos % 3)) && (nch > 0)) {
         toffs -- ;
         buf[toffs] = '\''; //inject the separator
      }
      pos ++ ;
   }
   buf += toffs;
   return buf;
}

用法:

{
   char     cbuf[32]; 
   uint64_t val = 0xFFFFFFFFFFFFFFFFll;

   printf("%s", th_sep_u64(val, cbuf));

   //result: 18'446'744'073'709'551'615
}

此致

也许“只是”添加一个新的 printf 说明符:

static int printf_arginfo_M(const struct printf_info *info, size_t n, int *argtypes, int *size) {

    if ( info->is_long_double ) {               // %llM
        size[0] = sizeof(long long);
        if ( n > 0 ) argtypes[0] = PA_INT | PA_FLAG_LONG_LONG;
    }
    else if ( info->is_long ) {                 // %lM
        size[0] = sizeof(long);
        if ( n > 0 ) argtypes[0] = PA_INT | PA_FLAG_LONG;
    }
    else {
        size[0] = sizeof(int);                  // %M
        if ( n > 0 ) argtypes[0] = PA_INT;
    }

    return 1;
}

static int printf_output_M(FILE *stream, const struct printf_info *info, const void *const args[])
{
    long long number;

    if ( info->is_long_double ) {               // %llM
        number = *(const long long*)(args[0]);
    }
    else if ( info->is_long ) {                 // %lM
        number = *(const long*)(args[0]);
    }
    else {                                      // %M
        number = *(const int*)(args[0]);
    }

    long long value = (number < 0) ? -number : number;
    int len;
    char buf[32];
    char *pos = &buf[31];
    int i = 0;

    *pos = '[=10=]';

    do {
        if ( (i % 3 == 0) && (i > 0) ) *--pos = '.';
        *--pos = '0' + value % 10;
        value /= 10;
        i++;
    } while (value > 0);

    if (number < 0) *--pos = '-';

    len = fprintf(stream, "%s", pos);

    return len;
}

用法:

register_printf_specifier('M', printf_output_M, printf_arginfo_M);

printf("%M\n", -1234567890);
printf("%lM\n", -1234567890123456789l);
printf("%llM\n", -1234567890123456789ll);

缺点是,gcc 会抱怨新的说明符,因此您可能希望禁用这些警告:

#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"