How to format a number with thousands separator in C/C++
Asked Answered
S

8

14

I am trying to do this simple task. Just to format a number using C or C++, but under Windows CE programming.

In this environment, neither inbue nor setlocale methods work.

Finally I did this with no success:

char szValue[10];
sprintf(szValue, "%'8d", iValue);

Any idea?

Stanhope answered 18/4, 2017 at 21:26 Comment(6)
If require a fixed format then doing it yourself is likely your best bet. If you need locale-sensitive presentation then I suspect that GetNumberFormat is the way to go.Myramyrah
,3C and C++ are different languages. Asking about both is like asking two questions in a single post, which is against site-rules. As you used the C function printf, C seems to be your focus. Said that: what have you found out? You should know we are not a coding service. What did you try yourself? Show your code and state wht did not work. Sad enough this has to be said.Torus
Of course C and C++ are different languages. If I posted this question asking about C or C++ only means that I will accept a solution in either C or C++. It doesn't matter since a C++ compiler compiles also C code. Got it? or do you want me to post 2 different questions with the same body but changing C with C++? Is it not a waste of time? a C++ programmer also knows how to program in pure C (normally). But never mind.. I have developed a function that uses std::string to accomplish this task and it works like a charm.Stanhope
Concerning C and C++ tags: A good answer in one language may not be compilable in the other. Example: my answer does not compile in C++. Since C99, C has diverged from what C++ can compile. With dual tagging, the post appears it may want a solution that works in both, even if not optimal for the languages individually. It is this lack of goal clarity that that attacks DV with dual tags post.Ziegfeld
Since you seem to be ok with using C++, you could use the ' like so: 123'456'789. It works since C++14 and could be placed anywhere between digits, even like 1'2'3'4.Kiowa
C++ Standards Committee: sort this out. It's embarrassing!Kylakylah
H
19

Here's one way - create a custom locale and imbue it with the appropriately customised facet:

#include <locale>
#include <iostream>
#include <memory>

struct separate_thousands : std::numpunct<char> {
    char_type do_thousands_sep() const override { return ','; }  // separate with commas
    string_type do_grouping() const override { return "\3"; } // groups of 3 digit
};

int main()
{
    int number = 123'456'789;
    std::cout << "default locale: " << number << '\n';
    auto thousands = std::make_unique<separate_thousands>();
    std::cout.imbue(std::locale(std::cout.getloc(), thousands.release()));
    std::cout << "locale with modified thousands: " << number << '\n';
}

expected output:

default locale: 123456789
locale with modified thousands: 123,456,789
Herve answered 18/4, 2017 at 21:42 Comment(2)
Change the line "auto thousands = std::make_unique<separate_thousands>();" to "auto thousands = std::unique_ptr<separate_thousands>(new separate_thousands());" for C++11.Nitrometer
This appears to work, only if number is declared as an int. Suppose I change it to std::string number = "123456789012345678901234567890"; both the cout statements produce identical results.Equidistance
U
3

These functions work in C++, for numbers in string, with or without decimals.

Next function not support negative string numbers or decimal separators, but it was very simple:

std::string quickAddThousandSeparators(std::string value, char thousandSep = ',')
{
    int len = value.length();
    int dlen = 3;

    while (len > dlen) {
        value.insert(len - dlen, 1, thousandSep);
        dlen += 4;
        len += 1;
    }
    return value;
}

Next function support negative string numbers and decimal separators:

std::string addThousandSeparators(std::string value, char thousandSep = ',', char decimalSep = '.', char sourceDecimalSep = '.')
{
    int len = value.length();
    int negative = ((len && value[0] == '-') ? 1: 0);
    int dpos = value.find_last_of(sourceDecimalSep);
    int dlen = 3 + (dpos == std::string::npos ? 0 : (len - dpos));

    if (dpos != std::string::npos && decimalSep != sourceDecimalSep) {
        value[dpos] = decimalSep;
    }

    while ((len - negative) > dlen) {
        value.insert(len - dlen, 1, thousandSep);
        dlen += 4;
        len += 1;
    }
    return value;
}

And gtest passed tests:

TEST (quickAddThousandSeparators, basicNumbers) {
    EXPECT_EQ("", quickAddThousandSeparators(""));
    EXPECT_EQ("1", quickAddThousandSeparators("1"));
    EXPECT_EQ("100", quickAddThousandSeparators("100"));
    EXPECT_EQ("1,000", quickAddThousandSeparators("1000"));
    EXPECT_EQ("10,000", quickAddThousandSeparators("10000"));
    EXPECT_EQ("100,000", quickAddThousandSeparators("100000"));
    EXPECT_EQ("1,000,000", quickAddThousandSeparators("1000000"));
    EXPECT_EQ("1,000,000,000", quickAddThousandSeparators("1000000000"));
    EXPECT_EQ("1,012,789,345,456,123,678,456,345,809", quickAddThousandSeparators("1012789345456123678456345809"));
}

TEST (quickAddThousandSeparators, changeThousandSeparator) {
    EXPECT_EQ("", quickAddThousandSeparators("",'.'));
    EXPECT_EQ("1", quickAddThousandSeparators("1",'.'));
    EXPECT_EQ("100", quickAddThousandSeparators("100", '.'));
    EXPECT_EQ("1.000", quickAddThousandSeparators("1000", '.'));
    EXPECT_EQ("1.000.000.000", quickAddThousandSeparators("1000000000", '.'));
    EXPECT_EQ("1.012.789.345.456.123.678.456.345.809", quickAddThousandSeparators("1012789345456123678456345809", '.'));
}

TEST (addThousandSeparators, basicNumbers) {
    EXPECT_EQ("", addThousandSeparators(""));
    EXPECT_EQ("1", addThousandSeparators("1"));
    EXPECT_EQ("100", addThousandSeparators("100"));
    EXPECT_EQ("1,000", addThousandSeparators("1000"));
    EXPECT_EQ("10,000", addThousandSeparators("10000"));
    EXPECT_EQ("100,000", addThousandSeparators("100000"));
    EXPECT_EQ("1,000,000", addThousandSeparators("1000000"));
    EXPECT_EQ("1,000,000,000", addThousandSeparators("1000000000"));
    EXPECT_EQ("1,012,789,345,456,123,678,456,345,809", addThousandSeparators("1012789345456123678456345809"));
}

TEST (addThousandSeparators, negativeBasicNumbers) {
    EXPECT_EQ("", addThousandSeparators(""));
    EXPECT_EQ("-1", addThousandSeparators("-1"));
    EXPECT_EQ("-100", addThousandSeparators("-100"));
    EXPECT_EQ("-1,000", addThousandSeparators("-1000"));
    EXPECT_EQ("-10,000", addThousandSeparators("-10000"));
    EXPECT_EQ("-100,000", addThousandSeparators("-100000"));
    EXPECT_EQ("-1,000,000", addThousandSeparators("-1000000"));
    EXPECT_EQ("-1,000,000,000", addThousandSeparators("-1000000000"));
    EXPECT_EQ("-1,012,789,345,456,123,678,456,345,809", addThousandSeparators("-1012789345456123678456345809"));
}

TEST (addThousandSeparators, changeThousandSeparator) {
    EXPECT_EQ("", addThousandSeparators("",'.'));
    EXPECT_EQ("-1", addThousandSeparators("-1",'.'));
    EXPECT_EQ("100", addThousandSeparators("100", '.'));
    EXPECT_EQ("-1.000", addThousandSeparators("-1000", '.'));
    EXPECT_EQ("-1.000.000.000", addThousandSeparators("-1000000000", '.'));
    EXPECT_EQ("1.012.789.345.456.123.678.456.345.809", addThousandSeparators("1012789345456123678456345809", '.'));
}

TEST (addThousandSeparators, changeDecimalSeparator) {
    EXPECT_EQ("0,5", addThousandSeparators("0.5",'.',','));
    EXPECT_EQ("0", addThousandSeparators("0",'.',','));
    EXPECT_EQ("-1,23", addThousandSeparators("-1.23",'.',','));
    EXPECT_EQ("100,56", addThousandSeparators("100.56", '.',','));
    EXPECT_EQ("-1.000,786", addThousandSeparators("-1000.786", '.',','));
    EXPECT_EQ("-1.000.000.000,4859", addThousandSeparators("-1000000000.4859", '.', ','));
    EXPECT_EQ("1.012.789.345.456.123.678.456.345.809,6785960", addThousandSeparators("1012789345456123678456345809.6785960", '.',','));
}

TEST (addThousandSeparators, changeSourceDecimalSeparator) {
    EXPECT_EQ("0,5", addThousandSeparators("0,5",'.',',',','));
    EXPECT_EQ("0", addThousandSeparators("0",'.',',',','));
    EXPECT_EQ("-1,23", addThousandSeparators("-1,23",'.',',',','));
    EXPECT_EQ("100,56", addThousandSeparators("100,56", '.',',',','));
    EXPECT_EQ("-1.000,786", addThousandSeparators("-1000,786", '.',',',','));
    EXPECT_EQ("-1.000.000.000,4859", addThousandSeparators("-1000000000,4859", '.', ',',','));
    EXPECT_EQ("1.012.789.345.456.123.678.456.345.809,6785960", addThousandSeparators("1012789345456123678456345809,6785960", '.',',',','));
}
Uro answered 23/1, 2019 at 21:44 Comment(2)
This does not work for cultures which put the separators after different digits. For example, the same number is written as "10,000,000" in the USA and "1,00,00,000" in India.Darwindarwinian
I don't know what the idea behind the format used in India is, but it's obviously not a thousands separator as had been asked by the OP.Trela
C
1

I use this:

string thousands_separator(long long k, string symbol=",") {
    int l, c, i;
    string fin, s, u, rev;
    bool min = false;
    fin = "";
    c = 0;
    if(k < -999){
        k *= -1;
        min = true;
    }
    s = to_string(k);
    if(k > 999){
        l = s.length() - 1;
        for (i = l; i >= 0; i--) {
            fin += s[i];
            c++;
            if(c%3 == 0){
                fin += symbol;
            }
        }
        rev = fin;
        fin = "";
        l = rev.length() - 1;
        for (i = l; i >= 0; i--) {
            fin += rev[i];
        }
        u = fin[0];
        if(u == symbol){
            fin.erase(fin.begin());
        }
        if(min){
            fin.insert(0, "-");
        }
        return fin;
    } else {
        return s;
    }
}
Crumhorn answered 21/3, 2021 at 9:49 Comment(0)
H
0

Here's another way, using manual manipulations:

#include <iostream>
#include <string>
#include <algorithm>

int main()
{
    int number = 123'456'789;
    auto src = std::to_string(number);
    auto dest = std::string();

    auto count = 3;
    for(auto i = src.crbegin() ; i != src.crend() ; ++i) {
        if (count == 0)
        {
            dest.push_back(',');
            count = 3;
        }
        if (count--) {
            dest.push_back(*i);
        }
    }
    std::reverse(dest.begin(), dest.end());

    std::cout << dest << '\n';
}
Herve answered 18/4, 2017 at 21:59 Comment(0)
S
0

Finally, I have developed my own function:

std::string CFormat::GetInteger(int iValue)
{
    std::string sValue;
    std::string sDot = ".";
    for(int iTmp = iValue; iTmp; iTmp /= 1000)
    {
        if ((int)(iTmp / 1000) == 0)
            sDot.clear();
        sValue = sDot + SSTR(iTmp % 1000) + sValue;
    }
    if (sValue.empty())
        sValue = "0";
    return sValue;
}

I think it's simpler and it does not depend on other classes other than std::string, which I know it will work in Windows Mobile device.

Stanhope answered 18/4, 2017 at 22:35 Comment(2)
NMDV: By using a do { ... } while (iTmp); to insure at least one iteration, the following if (sValue.empty()) test can be eliminated. Also I think this would have interspersed '-' with a value like -1234567.Ziegfeld
Thanks for the suggestion regarding the loop. And this is limited only to positive numbers.Stanhope
C
0

Option 1 - use Locale:

#include <fmt/format.h>
#include <locale>

std::locale::global(std::locale("en_US.UTF-8"));
int a = 1000000;
auto s = fmt::format("{:L}", a);  // 1,000,000
// or
fmt::println("{:L}", a);

Option 2 - use fmt::group_digits

auto s = fmt::group_digits(12345);  // 12,345
fmt::println("{}", s);
Chak answered 28/7, 2023 at 4:36 Comment(0)
Z
-1

Note: At the time this answer was submitted, the post was tagged C/C++. Now it is tagged C. I suspect it may change again.


Should you want to roll your own C solution which uses C99, the below forms the basis that works on my Windows gcc under various locales.

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

#define INT_STR_SIZE (CHAR_BIT*sizeof(int)*3/10 + 2)
#define INT_SEP_STR_SIZE (INT_STR_SIZE * 3/2 + 1)
#define INT_SEP(x) int_sep((char[INT_SEP_STR_SIZE]) { "" }, INT_SEP_STR_SIZE, x)

char *int_sep(char *s, size_t sz, int x) {
  struct lconv *locale_ptr = localeconv();
  const char *grouping = locale_ptr->grouping;
  char sep = locale_ptr->thousands_sep[0];

  if (sz > 0) {
    int x0 = x;
    char *ptr = s + sz;
    *--ptr = '\0';
    char count = 0;
    do {
      if (count >= grouping[0]) {
        *--ptr = sep;
        if (grouping[1]) grouping++;
        count = 0;
      }
      count++;
      //printf("%d %d <%s> %p\n", count, n, locale_ptr->grouping, (void*)locale_ptr);
      *--ptr = (char) (abs(x % 10) + '0');
    } while (x /= 10);
    if (x0 < 0) {
      *--ptr = '-';
    }
    memmove(s, ptr, (size_t) (&s[sz] - ptr));
  }
  return s;
}

main

int main(void) {
  puts(setlocale(LC_ALL,"en_US"));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_SEP_STR_SIZE), INT_SEP(12345678), INT_SEP(1234));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(-1), INT_SEP(0), INT_SEP(1));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_MIN), INT_SEP(INT_MIN+1), INT_SEP(INT_MAX));
  puts(setlocale(LC_ALL,"C"));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_MIN), INT_SEP(INT_MIN+1), INT_SEP(INT_MAX));
  puts(setlocale(LC_ALL,"it_IT"));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_MIN), INT_SEP(INT_MIN+1), INT_SEP(INT_MAX));
  puts(setlocale(LC_ALL,"as_IN"));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_MIN), INT_SEP(INT_MIN+1), INT_SEP(INT_MAX));
  puts(setlocale(LC_ALL,"be_BY"));
  printf(":%15s: :%15s: :%15s:\n", INT_SEP(INT_MIN), INT_SEP(INT_MIN+1), INT_SEP(INT_MAX));
  return 0;
}

Output

en_US
:             17: :     12,345,678: :          1,234:
:             -1: :              0: :              1:
: -2,147,483,648: : -2,147,483,647: :  2,147,483,647:
C
:    -2147483648: :    -2147483647: :     2147483647:
it_IT
: -2.147.483.648: : -2.147.483.647: :  2.147.483.647:
as_IN
:-2,14,74,83,648: :-2,14,74,83,647: : 2,14,74,83,647:
be_BY
: -2 147 483 648: : -2 147 483 647: :  2 147 483 647:
Ziegfeld answered 18/4, 2017 at 22:47 Comment(0)
E
-1

Why re-invent the wheel and not use functions that are provided for this? See GetNumberFormat.

Custom formatting can be done using the correct NUMBERFMT structure values. For example (pseudo-code):

//Display three digits after the decimal point.
.NumDigits = 3
//Display zeros after the decimal point.
.LeadingZero = 1
//Group every three digits to the left of the decimal.
.Grouping = 3
//Use a comma to as the decimal point (like they do in France and Spain).
.lpDecimalSep = ","
//Likewise, use a period as the grouping separator.
.lpThousandSep = "."
//Put the negative sign immediately after the number.
.NegativeOrder = 3
Eada answered 19/4, 2017 at 4:24 Comment(2)
Provide sample pleaseNationalism
This is a microsoft extension, not a C++ function.Ebon

© 2022 - 2024 — McMap. All rights reserved.