如何在C语言中使用逗号作为千位分隔符格式化数字?

105

在C语言中,如何将大数字格式化为例如 11234567891,123,456,789

我尝试使用 printf("%'10d\n", 1123456789),但这并不起作用。

你有什么建议吗?解决方案越简单越好。


2
只是提醒一下:printf()系列格式化IO函数的“千位分隔符”标志(单引号字符:')是一个非标准标志,仅受到少数库实现的支持。很遗憾它不是标准的。 - Michael Burr
1
它是区域依赖的。根据Linux man page所述,它查看LC_NUMERIC。然而,我不知道有哪个区域支持这个。 - Joey Adams
1
@Joey,将LC_NUMERIC区域设置为当前的""可以使得'在我的Mac和我刚刚检查过的Linux机器上正常工作。 - Carl Norum
请注意,POSIX 2008(2013)版本的printf()函数系列标准化了使用'(单引号或撇号)字符与十进制数格式转换规范,以指定应使用千位分隔符格式化数字。 - Jonathan Leffler
2
请注意,在默认的“C”语言环境中,非货币千位分隔符是未定义的,因此“%'d”在“C”语言环境中不会产生逗号。您需要设置一个具有适当的非货币千位分隔符的语言环境。通常,setlocale(LC_ALL, "");可以完成这项工作 - 除了空字符串之外,语言环境名称的其他值都是实现定义的。 - Jonathan Leffler
显示剩余2条评论
26个回答

106

如果您的printf支持'标志(根据POSIX 2008 printf()的要求),您可能只需适当设置您的区域设置即可实现此目的。例如:

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

int main(void)
{
    setlocale(LC_NUMERIC, "");
    printf("%'d\n", 1123456789);
    return 0;
}

然后构建并运行:

$ ./example 
1,123,456,789

在 Mac OS X 和 Linux(Ubuntu 10.10)上进行了测试。


1
我在嵌入式系统中对sprintf()进行了测试,但它无法工作(显然,因为正如你所说,它不支持'标志)。 - gbmhunter
1
值得一提的是,AtmelStudio的嵌入式系统printf()似乎不支持'修饰符。从头文件可以看出:版权所有... 2007 Joerg Wunsch ... 1993加州大学理事会,即BSD衍生版本。 - Bob Stein
setlocale(LC_NUMERIC, ""); 做到了这一点。没有这行代码,%'d 格式化符号无法按预期工作。 - Telvas
2
虽然这很方便,但您不一定想要更改此功能(setlocale)的状态。 - ideasman42
1
这似乎在AWS EC2 Ubuntu 20上无法工作(需要其他人确认) - étale-cohomology
显示剩余5条评论

52

你可以按照以下递归方法实现(如果使用二进制补码,请注意 INT_MIN,你需要额外的代码来处理):

void printfcomma2 (int n) {
    if (n < 1000) {
        printf ("%d", n);
        return;
    }
    printfcomma2 (n/1000);
    printf (",%03d", n%1000);
}

void printfcomma (int n) {
    if (n < 0) {
        printf ("-");
        n = -n;
    }
    printfcomma2 (n);
}

简要说明:

  • 用户调用printfcomma,并传入一个整数,对于特殊情况的负数,会简单地打印“-”并将数字转换为正数(这是无法使用INT_MIN的部分)。
  • 当您输入printfcomma2时,小于1,000的数字将只打印并返回。
  • 否则,递归将在上一层上被调用(因此 1,234,567 将被调用为 1,234,然后是 1),直到找到小于1,000的数字。
  • 然后该数字将被打印,并我们将沿着递归树向上走,打印逗号和下一个数字。

还有一种更简洁的版本,但在每个级别都需要检查负数的不必要处理(鉴于递归级别的限制,这并不重要)。这是一个完整的测试程序:

#include <stdio.h>

void printfcomma (int n) {
    if (n < 0) {
        printf ("-");
        printfcomma (-n);
        return;
    }
    if (n < 1000) {
        printf ("%d", n);
        return;
    }
    printfcomma (n/1000);
    printf (",%03d", n%1000);
}

int main (void) {
    int x[] = {-1234567890, -123456, -12345, -1000, -999, -1,
               0, 1, 999, 1000, 12345, 123456, 1234567890};
    int *px = x;
    while (px != &(x[sizeof(x)/sizeof(*x)])) {
        printf ("%-15d: ", *px);
        printfcomma (*px);
        printf ("\n");
        px++;
    }
    return 0;
}

输出结果为:

-1234567890    : -1,234,567,890
-123456        : -123,456
-12345         : -12,345
-1000          : -1,000
-999           : -999
-1             : -1
0              : 0
1              : 1
999            : 999
1000           : 1,000
12345          : 12,345
123456         : 123,456
1234567890     : 1,234,567,890

以下是一种迭代解决方案,适用于那些不信任递归的人(尽管递归唯一的问题通常是栈空间,但对于此处的情况不会成为问题,因为即使对于一个64位整数,它也只会有几层深度):

void printfcomma (int n) {
    int n2 = 0;
    int scale = 1;
    if (n < 0) {
        printf ("-");
        n = -n;
    }
    while (n >= 1000) {
        n2 = n2 + scale * (n % 1000);
        n /= 1000;
        scale *= 1000;
    }
    printf ("%d", n);
    while (scale != 1) {
        scale /= 1000;
        n = n2 / scale;
        n2 = n2  % scale;
        printf (",%03d", n);
    }
}

这两个都为 INT_MAX 生成 2,147,483,647


上面所有的代码都是用逗号分隔三位数字组,但你也可以使用其他字符,比如空格:

void printfspace2 (int n) {
    if (n < 1000) {
        printf ("%d", n);
        return;
    }
    printfspace2 (n/1000);
    printf (" %03d", n%1000);
}

void printfspace (int n) {
    if (n < 0) {
        printf ("-");
        n = -n;
    }
    printfspace2 (n);
}

13

这是一个非常简单的实现。该函数不包含任何错误检查,缓冲区大小必须由调用者验证。它也不能处理负数。这些改进留给读者自己去练习。

void format_commas(int n, char *out)
{
    int c;
    char buf[20];
    char *p;

    sprintf(buf, "%d", n);
    c = 2 - strlen(buf) % 3;
    for (p = buf; *p != 0; p++) {
       *out++ = *p;
       if (c == 1) {
           *out++ = ',';
       }
       c = (c + 1) % 3;
    }
    *--out = 0;
}

我喜欢这个,它使用sprintf而不是printf,这对于嵌入式系统非常有用。 - gbmhunter
1
非常不错,但需要进行一些小调整才能适用于负数。 - ideasman42
以下是关于编程的内容,需要翻译成中文。请返回已翻译的文本:(为负数提供支持的修改版 https://dev59.com/QHM_5IYBdhLWcg3wRw51#24795133) - ideasman42

10

天哪!我经常使用Linux上的gcc/g++和glibc,虽然 ' 运算符可能不是标准的,但我喜欢它的简洁性。

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

int main()
{
    int bignum=12345678;

    setlocale(LC_ALL,"");

    printf("Big number: %'d\n",bignum);

    return 0;
}

输出结果如下:

大数字:12,345,678

只需要记住在其中调用“setlocale”,否则它不会格式化任何内容。


3
遗憾的是,在Windows/gcc 4.9.2中似乎无法正常工作。 - rdtsc
1
真糟糕!我本来以为无论是哪个操作系统,任何平台上的gcc都会给出类似的结果。不过现在知道了,也算是好事吧。只是不知道为什么会这样。嗯…… - lornix
请注意,如果正在使用的C库不支持“'”标志,则无法获得所需的输出 - 这与编译器无关。编译器确保调用printf()的库函数使用格式字符串;由库函数来解释它。在Windows上,CRT库可能不提供所需的支持 - 无论使用哪个编译器都是如此。 - Jonathan Leffler
顺便提一下,这在fprintf调用中也适用,并且对于浮点数也有效。 - mike65535

4
也许一个有区域设置意识的版本会更有趣。
#include <stdlib.h>
#include <locale.h>
#include <string.h>
#include <limits.h>

static int next_group(char const **grouping) {
    if ((*grouping)[1] == CHAR_MAX)
        return 0;
    if ((*grouping)[1] != '\0')
        ++*grouping;
    return **grouping;
}

size_t commafmt(char   *buf,            /* Buffer for formatted string  */
                int     bufsize,        /* Size of buffer               */
                long    N)              /* Number to convert            */
{
    int i;
    int len = 1;
    int posn = 1;
    int sign = 1;
    char *ptr = buf + bufsize - 1;

    struct lconv *fmt_info = localeconv();
    char const *tsep = fmt_info->thousands_sep;
    char const *group = fmt_info->grouping;
    char const *neg = fmt_info->negative_sign;
    size_t sep_len = strlen(tsep);
    size_t group_len = strlen(group);
    size_t neg_len = strlen(neg);
    int places = (int)*group;

    if (bufsize < 2)
    {
ABORT:
        *buf = '\0';
        return 0;
    }

    *ptr-- = '\0';
    --bufsize;
    if (N < 0L)
    {
        sign = -1;
        N = -N;
    }

    for ( ; len <= bufsize; ++len, ++posn)
    {
        *ptr-- = (char)((N % 10L) + '0');
        if (0L == (N /= 10L))
            break;
        if (places && (0 == (posn % places)))
        {
            places = next_group(&group);
            for (int i=sep_len; i>0; i--) {
                *ptr-- = tsep[i-1];
                if (++len >= bufsize)
                    goto ABORT;
            }
        }
        if (len >= bufsize)
            goto ABORT;
    }

    if (sign < 0)
    {
        if (len >= bufsize)
            goto ABORT;
        for (int i=neg_len; i>0; i--) {
            *ptr-- = neg[i-1];
            if (++len >= bufsize)
                goto ABORT;
        }
    }

    memmove(buf, ++ptr, len + 1);
    return (size_t)len;
}

#ifdef TEST
#include <stdio.h>

#define elements(x) (sizeof(x)/sizeof(x[0]))

void show(long i) {
    char buffer[32];

    commafmt(buffer, sizeof(buffer), i);
    printf("%s\n", buffer);
    commafmt(buffer, sizeof(buffer), -i);
    printf("%s\n", buffer);
}


int main() {

    long inputs[] = {1, 12, 123, 1234, 12345, 123456, 1234567, 12345678 };

    for (int i=0; i<elements(inputs); i++) {
        setlocale(LC_ALL, "");
        show(inputs[i]);
    }
    return 0;
}

#endif

这确实存在一个错误(但我认为相当小)。在二进制补码硬件上,它无法正确地转换最负数,因为它试图使用N = -N;将负数转换为其等效正数。在二进制补码中,最大的负数没有对应的正数,除非您将其提升到较大的类型。解决此问题的一种方法是通过将数字提升为相应的无符号类型(但这有点复杂)。


我在 https://stackoverflow.com/q/44523855/2642059 上提出了一个关于跨平台实现格式 '-flag 的问题。我认为这个答案完美地解决了这个问题,现在正在进行更多的测试。如果是这样,我应该将那个问题标记为重复了,对吧? - Jonathan Mee
好的,首先我注意到的是它不随语言环境的变化而调整。为什么要维护tsepplace_strneg_str呢?为什么不直接使用fmt_info的成员呢? - Jonathan Mee
好的,第二个问题是,这段代码无法处理负数……而且我也不确定它该如何处理,“while (*ptr-- = *neg_str++)” 对我来说没有太多意义。你是在倒序插入负数字符串字符。 - Jonathan Mee
所以...我已经消除了内存泄漏,并纠正了负数的错误:http://ideone.com/gTv8Z4。不幸的是,仍然存在一个问题,即多个字符分隔符或多个字符负号被倒序写入字符串。我接下来会尝试解决这个问题... - Jonathan Mee
是的,这完美回答了我的问题。那么,我应该用这个答案的链接关闭我的问题吗?这个问题似乎对一个简单的解决方案感兴趣,而我的问题则涉及跨平台的解决方案。在我看来,它们有区别。但我会听取你的意见。如果你认为它是重复的,我会关闭它。如果你认为它不同,我会感激你复制粘贴你的答案。 - Jonathan Mee
显示剩余2条评论

2

不使用递归或字符串处理,采用数学方法:

#include <stdio.h>
#include <math.h>

void print_number( int n )
{
    int order_of_magnitude = (n == 0) ? 1 : (int)pow( 10, ((int)floor(log10(abs(n))) / 3) * 3 ) ;

    printf( "%d", n / order_of_magnitude ) ;

    for( n = abs( n ) % order_of_magnitude, order_of_magnitude /= 1000;
        order_of_magnitude > 0;
        n %= order_of_magnitude, order_of_magnitude /= 1000 )
    {
        printf( ",%03d", abs(n / order_of_magnitude) ) ;
    }
}

原理类似于Pax的递归解决方案,但通过提前计算数量级,避免了递归(可能会付出相当大的代价)。

还要注意,用于分隔千位的实际字符是与语言环境相关的。

编辑:请查看@Chux下面的评论以获取改进意见。


1
abs(n) 改为 fabs(n) 可以避免在执行 print_number(INT_MIN) 时出现二进制补码错误。 - chux - Reinstate Monica
@chux:说得好,但在%表达式中,左侧会被强制转换为int类型,仍然会出现问题。也许更简单的方法是接受略小的可接受输入范围,或者添加一个测试并直接输出“-2,147,483,647”以表示INT_MIN(或任何平台上的INT_MIN - 这是另一个问题)。 - Clifford
我在建议之前已经成功测试过了。嗯,我发现我的想法只适用于 log10(abs(n)) 而不是其他地方。有趣的是,你的解决方案只需要将 log10(fabs(n))print_number(INT_MIN) 进行单一更改就可以工作了,因为 printf(..., abs(n / order_of_magnitude)) 意味着 n = abs(INT_MIN) % order_of_magnitude 是负数也没关系。如果我们放弃 INT_MIN,那么 printf(..., abs(n / order_of_magnitude)) 可以变成 printf(..., n / order_of_magnitude)。但我想,与 "abs(INT_MIN)" 打交道通常是一件坏事。 - chux - Reinstate Monica
新想法:建议三个更改 log10(fabs(n))n = abs(n% order_of_magnitude)printf(",%03d", n/order_of_magnitude)。顺便说一句:我不会花费这种努力,除非我认为你的解决方案很好。即使是 INT_MIN,也没有 UB。 - chux - Reinstate Monica

2
另一个解决方案是将结果保存到一个 int 数组中,最大长度为 7,因为 long long int 类型可以处理范围在 9,223,372,036,854,775,807 到 -9,223,372,036,854,775,807 的数字(注意它不是无符号值)。
非递归打印函数。
static void printNumber (int numbers[8], int loc, int negative)
{
    if (negative)
    {
        printf("-");
    }
    if (numbers[1]==-1)//one number
    {
        printf("%d ", numbers[0]);
    }
    else
    {
        printf("%d,", numbers[loc]);
        while(loc--)
        {
            if(loc==0)
            {// last number
                printf("%03d ", numbers[loc]);
                break;
            }
            else
            { // number in between
                printf("%03d,", numbers[loc]);
            }
        }
    }
}

主函数调用
static void getNumWcommas (long long int n, int numbers[8])
{
    int i;
    int negative=0;
    if (n < 0)
    {
        negative = 1;
        n = -n;
    }
    for(i = 0; i < 7; i++)
    {
        if (n < 1000)
        {
            numbers[i] = n;
            numbers[i+1] = -1;
            break;
        }
        numbers[i] = n%1000;
        n/=1000;
    }

    printNumber(numbers, i, negative);// non recursive print
}

测试输出

-9223372036854775807: -9,223,372,036,854,775,807
-1234567890         : -1,234,567,890
-123456             : -123,456
-12345              : -12,345
-1000               : -1,000
-999                : -999
-1                  : -1
0                   : 0
1                   : 1
999                 : 999
1000                : 1,000
12345               : 12,345
123456              : 123,456
1234567890          : 1,234,567,890
9223372036854775807 : 9,223,372,036,854,775,807

在main()函数中:
int numberSeparated[8];
long long int number = 1234567890LL;
getNumWcommas(number, numberSeparated);

如果仅需要打印,则将 int numberSeparated[8]; 移动到函数 getNumWcommas 内,并以以下方式调用该函数 getNumWcommas(number)

2

基于@Greg Hewgill的代码,但考虑了负数并返回字符串大小。

size_t str_format_int_grouped(char dst[16], int num)
{
    char src[16];
    char *p_src = src;
    char *p_dst = dst;

    const char separator = ',';
    int num_len, commas;

    num_len = sprintf(src, "%d", num);

    if (*p_src == '-') {
        *p_dst++ = *p_src++;
        num_len--;
    }

    for (commas = 2 - num_len % 3;
         *p_src;
         commas = (commas + 1) % 3)
    {
        *p_dst++ = *p_src++;
        if (commas == 1) {
            *p_dst++ = separator;
        }
    }
    *--p_dst = '\0';

    return (size_t)(p_dst - dst);
}

2

我自己也需要做类似的事情,但不是直接打印,而是需要进入缓冲区。以下是我想出来的方法。从后往前工作。

unsigned int IntegerToCommaString(char *String, unsigned long long Integer)
{
    unsigned int Digits = 0, Offset, Loop;
    unsigned long long Copy = Integer;

    do {
        Digits++;
        Copy /= 10;
    } while (Copy);

    Digits = Offset = ((Digits - 1) / 3) + Digits;
    String[Offset--] = '\0';

    Copy = Integer;
    Loop = 0;
    do {
        String[Offset] = '0' + (Copy % 10);
        if (!Offset--)
            break;
        if (Loop++ % 3 == 2)
            String[Offset--] = ',';
        Copy /= 10;
    } while (1);

    return Digits;
}

请注意,此代码仅适用于无符号整数,并且您必须确保缓冲区足够大。


1
#include <stdio.h>

void punt(long long n){
    char s[28];
    int i = 27;
    if(n<0){n=-n; putchar('-');} 
    do{
        s[i--] = n%10 + '0';
        if(!(i%4) && n>9)s[i--]='.';
        n /= 10;
    }while(n);
    puts(&s[++i]);
}


int main(){
    punt(2134567890);
    punt(987);
    punt(9876);
    punt(-987);
    punt(-9876);
    punt(-654321);
    punt(0);
    punt(1000000000);
    punt(0x7FFFFFFFFFFFFFFF);
    punt(0x8000000000000001); // -max + 1 ...
}

我的解决方案使用点号代替逗号,读者需要自行更改。

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