Foreword: I released this utility with more customization in github.com/icza/gox
, see fmtx.FormatInt()
.
The fmt
package does not support grouping decimals.
We have to implement one ourselves (or use an existing one).
The Code
Here is a compact and really efficient solution (see explanation after):
Try it on the Go Playground.
func Format(n int64) string {
in := strconv.FormatInt(n, 10)
numOfDigits := len(in)
if n < 0 {
numOfDigits-- // First character is the - sign (not a digit)
}
numOfCommas := (numOfDigits - 1) / 3
out := make([]byte, len(in)+numOfCommas)
if n < 0 {
in, out[0] = in[1:], '-'
}
for i, j, k := len(in)-1, len(out)-1, 0; ; i, j = i-1, j-1 {
out[j] = in[i]
if i == 0 {
return string(out)
}
if k++; k == 3 {
j, k = j-1, 0
out[j] = ','
}
}
}
Testing it:
for _, v := range []int64{0, 1, 12, 123, 1234, 123456789} {
fmt.Printf("%10d = %12s\n", v, Format(v))
fmt.Printf("%10d = %12s\n", -v, Format(-v))
}
Output:
0 = 0
0 = 0
1 = 1
-1 = -1
12 = 12
-12 = -12
123 = 123
-123 = -123
1234 = 1,234
-1234 = -1,234
123456789 = 123,456,789
-123456789 = -123,456,789
Explanation:
Basically what the Format()
function does is it formats the number without grouping, then creates a big enough other slice and copies the digits of the number inserting comma (','
) grouping symbol when necessary (after groups of digits of 3 if there are more digits) meanwhile taking care of the negative sign to be preserved.
The length of the output:
It is basically the length of the input plus the number of grouping signs to be inserted. The number of grouping signs is:
numOfCommas = (numOfDigits - 1) / 3
Since the input string is a number which may only contain digits ('0..9'
) and optionally a negative sign ('-'
), the characters are simply mapped to bytes in a 1-to-1 fashion in UTF-8 encoding (this is how Go stores strings in memory). So we can simply work with bytes instead of runes. So the number of digits is the input string length, optionally minus 1
if the number is negative:
numOfDigits := len(in)
if n < 0 {
numOfDigits-- // First character is the - sign (not a digit)
}
And therefore the number of grouping signs:
numOfCommas := (numOfDigits - 1) / 3
Therefore the output slice will be:
out := make([]byte, len(in)+numOfCommas)
Handling the negative sign character:
If the number is negative, we simply slice the input string to exclude it from processing and we manually copy the sign bit to the output:
if n < 0 {
in, out[0] = in[1:], '-'
}
And therefore the rest of the function does not need to know/care about the optional negative sign character.
The rest of the function is a for
loop which just copies the bytes (digits) of the number from the input string to the output, inserting a grouping sign (','
) after every group of 3 digits if there are more digits. The loop goes downward so it's easier to track the groups of 3 digits. Once done (no more digits), the output byte slice is returned as a string
.
Variations
Handling negative with recursion
If you're less concerned with efficiency and more about readability, you might like this version:
func Format2(n int64) string {
if n < 0 {
return "-" + Format2(-n)
}
in := strconv.FormatInt(n, 10)
numOfCommas := (len(in) - 1) / 3
out := make([]byte, len(in)+numOfCommas)
for i, j, k := len(in)-1, len(out)-1, 0; ; i, j = i-1, j-1 {
out[j] = in[i]
if i == 0 {
return string(out)
}
if k++; k == 3 {
j, k = j-1, 0
out[j] = ','
}
}
}
Basically this handles negative numbers with a recursive call: if the number is negative, calls itself (recursive) with the absolute (positive) value and prepends the result with a "-"
string.
With append()
slices
Here's another version using the builtin append()
function and slice operations. Somewhat easier to understand but not so good performance-wise:
func Format3(n int64) string {
if n < 0 {
return "-" + Format3(-n)
}
in := []byte(strconv.FormatInt(n, 10))
var out []byte
if i := len(in) % 3; i != 0 {
if out, in = append(out, in[:i]...), in[i:]; len(in) > 0 {
out = append(out, ',')
}
}
for len(in) > 0 {
if out, in = append(out, in[:3]...), in[3:]; len(in) > 0 {
out = append(out, ',')
}
}
return string(out)
}
The first if
statement takes care of the first optional, "incomplete" group which is less than 3 digits if exists, and the subsequent for
loop handles the rest, copying 3 digits in each iteration and appending a comma (','
) grouping sign if there are more digits.