为 C printf 函数设置千位分隔符

15

我有这样一段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 设置为 ',它也会被忽略。有办法设置任何字符作为千位分隔符吗?


你不需要在修改本地结构后调用 useLocale 方法,以便将新数据传递到运行时吗? - caveman
http://stackoverflow.com/a/24629137/1606345 - David Ranieri
好的,那就不可能了。我会继续使用我的自定义函数,它既不使用malloc,也是线程安全的。 - Peter VARGA
1
我认为对于C语言来说,编写自己的格式化函数是一个不错的选择。 - VolAnd
@VolAnd 我找到了一个新的更好的解决方案,它在Linux上使用C语言,并且我已经将其发布为一个新的答案。 - Peter VARGA
6个回答

3
这里有一个非常简单的解决方案,适用于每个Linux发行版,不需要像我的第一个答案一样进行glibc的修改:
所有这些步骤都必须在原始的glibc目录中执行 - 而不是在构建目录中 - 在使用单独的构建目录构建glibc版本后,按照此说明建议进行操作。
我的新locale文件名为en_AT
  1. 从现有文件en_USlocaledata/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_ATLC_NUMERIC设置。


2

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 "\3"; }
};

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

编辑2:

C++参考文献中提到:

localeconv()返回一个填充好的struct lconv对象指针。对象中包含的值可以被后续调用localeconv覆盖,并且不会直接修改该对象。使用类别值为LC_ALL、LC_MONETARY或LC_NUMERIC的setlocale调用会覆盖结构体的内容。

我在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

因此,可以假设通过使用localeconv()接收的指针可以改变decimal_pointthousands_sep的值,但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 "\3"; }
};

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*
}

但是printf()正在访问哪个结构?一定有一种方法可以覆盖千字符。我在GNU glib库中挖掘了printf(),但它并没有硬编码! - Peter VARGA
此外,还要检查 snprintf 是否是与语言环境相关的函数。 - VolAnd
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Peter VARGA
@AlBundy: 我认为,“printf”系列函数只是忽略了ptrLocale->thousands_sep的设置。请参见EDIT 2 - VolAnd
顺便提一下,在我的测试中,我没有使用strcpy来设置小数点或千位分隔符,而是简单地使用ptrLocale->decimal_point =“:”。如果千位分隔符是空字符串,则使用非空源的strcpy可能会导致缓冲区溢出。 - Serge Ballesta
显示剩余8条评论

1

这个答案是从VolAnd的答案衍生而来。

根据此来源,千分位分隔符仅在使用非标准'标记时才使用。

因此,如果你的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;

这是我提问中的代码,我想知道该如何实现。你的代码对我不起作用。我必须使用 setlocale(LC_NUMERIC,“en_US”); 才能看到美国千位分隔符字符。 - Peter VARGA
@AlBundy:使用这段代码,我成功地更改了十进制分隔符。不幸的是,在我尝试过的两个系统中,无论我使用哪种语言环境,都不支持 ' 非标志。 - Serge Ballesta
请注意,它是否符合标准取决于您选择的标准。 POSIX指定printf()支持'表示应适当打印千位分隔符。此外,Mac OS X(10.10.5)和推断BSD具有一组_l打印函数:例如,int printf_l(locale_t loc, const char * restrict format, ...);int fprintf_l(FILE * restrict stream, locale_t loc, const char * restrict format, ...);如果可用,则这些是最佳选择。 - Jonathan Leffler
更严重的是,需要注意 POSIX 规范中对 localeconv() 的明确说明:_localeconv() 函数不一定是线程安全的。... localeconv() 函数应返回指向填充对象的指针。应用程序不得修改返回值所指向的结构,也不得修改结构内部指针所指向的任何存储区域。_ 一个可移植的应用程序可能无法像这个答案建议的那样做。 - Jonathan Leffler

0
也许只需要添加一个新的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 = '\0';

    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"

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. setMyThousandSeparator.o与您的项目链接。

目前我尝试了在静态链接libc时它是有效的。

setMyThousandSeparator.c的内容:

#include <locale/localeinfo.h>

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

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


0
这是一个我用于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
}

敬礼


这是一个不错的函数,但当您的格式字符串包含许多不同的格式时,它会变得很繁琐。重新编译GNU glibc确实可能变得非常棘手,我不认为这是一个好的解决方案-5年后。现在,我通过构建一个新的LOCALE来解决这个问题,这实际上是一个非常简单的任务,巨大的优势是千位分隔符甚至可以在Linux Bash命令行中工作,因此它变成了全局的。 - Peter VARGA
是的,这完全取决于您需要实现什么。 我需要在许多计算机和不同的操作系统上运行我的代码,因此对我来说,在每种情况下都构建/安装/更改 LOCALE 是很繁琐的。 - vtomazzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接