Digit grouping is not a standard feature in the printf family of functions in Standard C. It is a POSIX extension supported by the GNU libc and other unix libraries. It applies to decimal integer conversions (%i, %d and %u) and the integral portion of floating point conversions (%f, %g and %G).
The number of digits in each group cannot be specified on an conversion basis nor depending on the base or the group number, it is specified in the locale.
The thousands separator is specified in the locale as well as the decimal separator that will be used for floating point conversions. The purpose is the conversion of currency amounts consistent with the local culture. Note that in some cases, the number of digits is not the same for all groups (eg: the Indian numbering system), so this feature is only half-baked.
This feature does not meet your goal as it does not apply to hexadecimal or binary conversions and modifying the locale definition is risky anyway.
Here is a simple function to format integers in different bases where you can specify both the grouping number and the separator.
#include <limits.h>
#include <stdio.h>
/* Convert an integer with parameterized grouping
* return -1 if base is invalid
* returns the length of the output without truncation
*/
int format_ull(char *dest, /* destination array */
size_t size, /* array length */
unsigned long long n, /* value to convert */
int base, /* output radix, 0 or 2..36 */
int mindigits, /* minimum number of digits */
int grouping, /* group length, 0 for no groups */
int sep) /* separator character */
{
char buf[sizeof(n) * CHAR_BIT];
char *p = buf + sizeof(buf);
const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
int ndigits, nzeroes, phase;
size_t pos, len;
if (base < 2 || base > 36) {
if (base == 0)
base = 10;
else
return -1;
}
while (n) {
*--p = digits[n % (unsigned)base];
n = n / (unsigned)base;
}
ndigits = buf + sizeof(buf) - p;
nzeroes = mindigits > ndigits ? mindigits - ndigits : 0;
ndigits += nzeroes;
if (grouping <= 0 || ndigits <= grouping) {
len = phase = ndigits;
} else {
phase = (ndigits + grouping - 1) % grouping + 1;
len = ndigits + (ndigits - 1) / grouping;
}
if (size > 0) {
size--;
if (size > len)
size = len;
for (pos = 0; pos < size; pos++) {
if (phase-- > 0) {
dest[pos] = (char)((nzeroes-- > 0) ? '0' : *p++);
} else {
phase = grouping;
dest[pos] = (char)sep;
}
}
dest[pos] = '\0';
}
return (int)len;
}
#define TEST(n) test(n, #n)
void test(unsigned long long n, const char *source) {
char buf[100];
int nbits = sizeof(n) * CHAR_BIT;
int len;
printf("source: %s\n", source);
printf("normal: %llu\n", n);
format_ull(buf, sizeof buf, n, 10, 1, 3, ',');
printf("base 10/3: %s\n", buf);
len = format_ull(buf, sizeof buf, n, 8, 1, 3, '\'');
printf("base 8/3: %.*s%s\n", (*buf != '0') * (1 + (len % 4 == 3)), "0'", buf);
format_ull(buf, sizeof buf, n, 16, nbits / 4, 4, '\'');
printf("base 16/4: 0x%s\n", buf);
format_ull(buf, sizeof buf, n, 2, nbits, 8, '\'');
printf("base 2/8: 0b%s\n", buf);
printf("\n");
}
int main(void) {
TEST(0);
TEST(ULLONG_MAX);
TEST(0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001);
return 0;
}
Output:
source: 0
normal: 0
base 10/3: 0
base 8/3: 0
base 16/4: 0x0000'0000'0000'0000
base 2/8: 0b00000000'00000000'00000000'00000000'00000000'00000000'00000000'00000000
source: ULLONG_MAX
normal: 18446744073709551615
base 10/3: 18,446,744,073,709,551,615
base 8/3: 01'777'777'777'777'777'777'777
base 16/4: 0xffff'ffff'ffff'ffff
base 2/8: 0b11111111'11111111'11111111'11111111'11111111'11111111'11111111'11111111
source: 0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001
normal: 1234567890987654321
base 10/3: 1,234,567,890,987,654,321
base 8/3: 0'104'420'417'226'133'016'261
base 16/4: 0x1122'10f4'b16c'1cb1
base 2/8: 0b00010001'00100010'00010000'11110100'10110001'01101100'00011100'10110001
For your specific purpose, here is a simpler version for binary output grouped in sets of 4 bits. It uses a static buffer so it is not reentrant and can only be used once per printf call:
#include <limits.h>
#include <stdio.h>
/* Convert an integer to binary, grouping digits in sets of 4
*/
const char *format_bin4(int prefix, int min_digits, unsigned long long n) {
static char buf[2 + sizeof(n) * CHAR_BIT * 5 / 4 + 1];
char *p = buf + sizeof(buf);
int group = 4;
int i;
if (n == 0) prefix--;
*--p = '\0';
for (i = 0; p > buf + 2 && (i < min_digits || n != 0); i++) {
if (!group--) {
*--p = '\'';
group = 3;
}
*--p = '0' + (n & 1);
n >>= 1;
}
if (prefix > 0) {
*--p = 'b';
*--p = '0';
}
return p;
}
int main(void) {
unsigned long long x = 0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001;
const char *x_source = "0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001";
printf(" 0, 0: %s\n", format_bin4(1, 0, 0));
printf(" 0, 1: %s\n", format_bin4(1, 1, 0));
printf(" 0, 2: %s\n", format_bin4(1, 2, 0));
printf(" 0, 64: %s\n", format_bin4(1, 64, 0));
printf("ULLONG_MAX, 64: %s\n", format_bin4(1, 64, ULLONG_MAX));
printf(" x source, 64: %s\n", x_source);
printf(" x format, 64: %s\n", format_bin4(1, 64, x));
return 0;
}
Output:
0, 0:
0, 1: 0
0, 2: 00
0, 64: 0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000'0000
ULLONG_MAX, 64: 0b1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111'1111
x source, 64: 0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001
x format, 64: 0b0001'0001'0010'0010'0001'0000'1111'0100'1011'0001'0110'1100'0001'1100'1011'0001
.groupingof the locale from 3 to 4._approach, which is more readable. But this is C.'printf flag isn't a C23 feature.