How to fmt.Printf an integer with thousands comma

2019-01-17 18:59发布

问题:

Does Go's fmt.Printf support outputting a number with the thousands comma?

fmt.Printf("%d", 1000) outputs 1000, what format can I specify to output 1,000 instead?

The docs don't seem to mention commas, and I couldn't immediately see anything in the source.

回答1:

None of the fmt print verbs support thousands separators.



回答2:

I wrote a library for this as well as a few other human-representation concerns.

Example results:

0 -> 0
100 -> 100
1000 -> 1,000
1000000000 -> 1,000,000,000
-100000 -> -100,000

Example Usage:

fmt.Printf("You owe $%s.\n", humanize.Comma(6582491))


回答3:

Use golang.org/x/text/message to print using localized formatting for any language in the Unicode CLDR:

package main

import (
    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

func main() {
    p := message.NewPrinter(language.English)
    p.Printf("%d\n", 1000)

    // Output:
    // 1,000
}


回答4:

I published a Go snippet over at Github of a function to render a number (float64 or int) according to user-specified thousand separator, decimal separator and decimal precision.

https://gist.github.com/gorhill/5285193

Usage: s := RenderFloat(format, n)

The format parameter tells how to render the number n.

Examples of format strings, given n = 12345.6789:

"#,###.##" => "12,345.67"
"#,###." => "12,345"
"#,###" => "12345,678"
"#\u202F###,##" => "12 345,67"
"#.###,###### => 12.345,678900
"" (aka default format) => 12,345.67


回答5:

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)
    out := make([]byte, len(in)+(len(in)-2+int(in[0]/'0'))/3)
    if in[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 for the sign digit '-'.

If there is a sign digit, it will be in in[0]. The numerical value of '-' is 45, while the numerical value of digit characters '0'..'9' are 48..57. So the sign character is less than the possible digits. So if we divide the first character (there is always at least 1 character) by '0', we get 0 if it is a negative sign and 1 if it is a digit (integer division).

So the number of digits in the input string is:

numOfDigits = len(in) - 1 + int(in[0]/'0')

And therefore the number of grouping signs:

numOfCommas = (len(in) - 2 + int(in[0]/'0')) / 3

Therefore the output slice will be:

out := make([]byte, len(in)+(len(in)-2+int(in[0]/'0'))/3)

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 in[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)
    out := make([]byte, len(in)+(len(in)-1)/3)

    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.



回答6:

Here is a function that takes an integer and grouping separator and returns a string delimited with the specified separator. I have tried to optimize for efficiency, no string concatenation or mod/division in the tight loop. From my profiling it is more than twice as fast as the humanize.Commas implementation (~680ns vs 1642ns) on my Mac. I am new to Go, would love to see faster implementations!

Usage: s := NumberToString(n int, sep rune)

Examples

Illustrates using different separator (',' vs ' '), verified with int value range.

s:= NumberToString(12345678, ',')

=> "12,345,678"

s:= NumberToString(12345678, ' ')

=> "12 345 678"

s: = NumberToString(-9223372036854775807, ',')

=> "-9,223,372,036,854,775,807"

Function Implementation

func NumberToString(n int, sep rune) string {

    s := strconv.Itoa(n)

    startOffset := 0
    var buff bytes.Buffer

    if n < 0 {
        startOffset = 1
        buff.WriteByte('-')
    }


    l := len(s)

    commaIndex := 3 - ((l - startOffset) % 3) 

    if (commaIndex == 3) {
        commaIndex = 0
    }

    for i := startOffset; i < l; i++ {

        if (commaIndex == 3) {
            buff.WriteRune(sep)
            commaIndex = 0
        }
        commaIndex++

        buff.WriteByte(s[i])
    }

    return buff.String()
}


回答7:

Here's a simple function using regex:

import (
    "strconv"
    "regexp"
)

func formatCommas(num int) string {
    str := strconv.Itoa(num)
    re := regexp.MustCompile("(\\d+)(\\d{3})")
    for i := 0; i < (len(str) - 1) / 3; i++ {
        str = re.ReplaceAllString(str, "$1,$2")
    }
    return str
}

Example:

fmt.Println(formatCommas(1000))
fmt.Println(formatCommas(-1000000000))

Output:

1,000
-1,000,000,000

https://play.golang.org/p/0v6wOzxJ1H



回答8:

Use https://github.com/dustin/go-humanize .. it has a bunch of helpers to deal with those things. In addition to bytes as MiB, MB, and other goodies.



回答9:

If you don't want to use a library (for whatever reason), I knocked this up. It seems to work and can use any specified rune as a delimiter:

import (
    "strconv"
)

func delimitNumeral(i int, delim rune) string {

    src := strconv.Itoa(i)
    strLen := utf8.RuneCountInString(src)
    outStr := ""
    digitCount := 0
    for i := strLen - 1; i >= 0; i-- {

        outStr = src[i:i+1] + outStr
        if digitCount == 2 {
            outStr = string(delim) + outStr
            digitCount = 0
        } else {
            digitCount++
        }
    }

    return outStr
}

Note: after further testing, this function doesn't work perfectly. I would suggest using the solution posted by @IvanTung, and welcome any edits from anyone who can get mine to work perfectly.



回答10:

import ("fmt"; "strings")

func commas(s string) string {
    if len(s) <= 3 {
        return s
    } else {
        return commas(s[0:len(s)-3]) + "," + s[len(s)-3:]
    }
}

func toString(f float64) string {
    parts := strings.Split(fmt.Sprintf("%.2f", f), ".")
    if parts[0][0] == '-' {
        return "-" + commas(parts[0][1:]) + "." + parts[1]
    }
    return commas(parts[0]) + "." + parts[1]
}


回答11:

The package humanize can do the magic! Refer the documentation of this package here. To use this package, install it first by using a tool like Git SCM. If you are using Git Bash, open the shell window and type:

go get -u github.com/dustin/go-humanize

Once this is done, you can use the following solution code (Say, main.go):

package main

import (
    "fmt"
    "github.com/dustin/go-humanize"
)

func main() {
    fmt.Println(humanize.Commaf(float64(123456789)));
    fmt.Println(humanize.Commaf(float64(-1000000000)));
    fmt.Println(humanize.Commaf(float64(-100000.005)));
    fmt.Println(humanize.Commaf(float64(100000.000)));
}

There are other variations to Commaf like BigComma, Comma, BigCommaf etc. which depends on the data type of your input.

So, on running this program using the command:

go run main.go

You will see an output such as this:

123,456,789
-1,000,000,000
-100,000.005
100,000