【问题标题】:Set thousands separator for C printf为 C printf 设置千位分隔符
【发布时间】:2015-02-26 14:11:22
【问题描述】:

我有这个 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将新数据传递到运行时吗?
  • 好的。那么这是不可能的。我一直在使用我自己的函数,它不使用 malloc 并且也是线程安全的。
  • 我认为对于 C 而言,决定编写自己的格式化函数是个好主意。
  • @VolAnd 我找到了一个新的、更好的解决方案,它可以在 linux 上的 C 中运行,我将它作为新答案发布。

标签: c printf locale


【解决方案1】:

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


所有这些步骤必须在 origin glibc 目录中执行 - NOT 在构建目录中 - 在您使用单独的构建目录构建 glibc 版本之后正如instructions所建议的那样。

我的新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
    

    因此,可以假设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 "\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(),它没有在那里硬编码!
    • 我想你需要setlocale()函数来改变当前的语言环境
    • 另外,检查snprintf是否是依赖于语言环境的函数
    • @VoIAnd:是的,但是如何明确设置另一个千位分隔符?使用预定义的语言环境“en_US”、“de_DE”……调用setlocale() 使用为语言环境定义的分隔符。
    • @AlBundy :我想,printf-family 函数只是忽略了ptrLocale-&gt;thousands_sep 设置。请参阅编辑 2
    【解决方案3】:

    有一个非常肮脏的 hack 如何更改 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 源代码调用 setMyThousandSeparator("'") 函数中,在 printf() 调用之前。
    8. setMyThousandSeparator.o 链接到您的项目。

    目前我在链接libc static 时尝试过,但它可以工作。

    setMyThousandSeparator.c的内容:

    #include <locale/localeinfo.h>
    
    void setMyThousandSeparator(char * sMySeparator)
    {
        _NL_CURRENT (LC_NUMERIC, THOUSANDS_SEP) = sMySeparator;
    }
    

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

    【讨论】:

      【解决方案4】:

      这个答案来自VolAnd's one

      根据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;
      

      【讨论】:

      • 这是我的问题中的代码,我在问如何做到这一点。你的代码对我不起作用。我必须使用setlocale(LC_NUMERIC, "en_US"); 才能看到至少美国千位分隔符。
      • @AlBundy :使用此代码,我可以成功更改小数点分隔符。不幸的是,我尝试过的两个系统不支持 ' 非标准标志(无论我使用什么语言环境)。
      • 请注意,它是否为标准取决于您选择的标准。 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, ...);。如果有的话,这些是最好的选择。
      • 更严重的是,请注意localeconv() 的 POSIX 规范明确指出:localeconv() 函数不必是线程安全的。 …localeconv() 函数应返回一个指向填充对象的指针。应用程序不得修改返回值指向的结构,也不得修改结构内指针指向的任何存储区域。 可移植应用程序可能不会按照此答案的建议进行操作。
      【解决方案5】:

      这是一个专门用于 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 命令行中也可以工作,因此它变成了全局的。
      • 是的,这完全取决于您需要实现的目标。我需要在许多机器、不同的操作系统上运行我的代码,所以对我来说,为每种情况构建/安装/更改 LOCALE 将是烦人
      【解决方案6】:

      也许“只是”添加一个新的 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"
      

      【讨论】:

        猜你喜欢
        • 2011-08-17
        • 1970-01-01
        • 1970-01-01
        • 2011-12-06
        • 1970-01-01
        • 2011-07-16
        • 1970-01-01
        • 2016-01-16
        相关资源
        最近更新 更多