How to fmt.Printf an integer with thousands comma

后端 未结 13 1807
独厮守ぢ
独厮守ぢ 2020-12-04 21:21

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

fmt.Printf(\"%d\", 1000) outputs 1000, what format

相关标签:
13条回答
  • 2020-12-04 21:48

    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.

    0 讨论(0)
  • 2020-12-04 21:49

    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))
    
    0 讨论(0)
  • 2020-12-04 21:56

    None of the fmt print verbs support thousands separators.

    0 讨论(0)
  • 2020-12-04 21:58

    I got interested in the performance of solutions offered in earlier answers and wrote tests with benchmarks for them, including two code snippets of mine. The following results were measured on MacBook 2018, i7 2.6GHz:

    +---------------------+-------------------------------------------+--------------+
    |       Author        |                Description                |    Result    |
    |---------------------|-------------------------------------------|--------------|
    | myself              | dividing by 1,000 and appending groups    |  3,472 ns/op |
    | myself              | inserting commas to digit groups          |  2,662 ns/op |
    | @icza               | collecting digit by digit to output array |  1,695 ns/op |
    | @dolmen             | copying digit groups to output array      |  1,797 ns/op |
    | @Ivan Tung          | writing digit by digit to buffer          |  2,753 ns/op |
    | @jchavannes         | inserting commas using a regexp           | 63,995 ns/op |
    | @Steffi Keran Rani, | using github.com/dustin/go-humanize       |  3,525 ns/op |
    |  @abourget, @Dustin |                                           |              |
    | @dolmen             | using golang.org/x/text/message           | 12,511 ns/op |
    +---------------------+-------------------------------------------+--------------+
    
    • If you want the fastest solution, grab @icza's code snippet. Although it goes digit by digit and not by groups of three digits, it emerged as the fastest.
    • If you want the shortest reasonable code snippet, look at mine below. It adds more than half of the time of the fastest solution, but the code is three times shorter.
    • If you want a one-liner and do not mind using an external library, go for github.com/dustin/go-humanize. It is more than twice slower as the fastest solution, but the library might help you with other formatting.
    • If you want localized output, choose golang.org/x/text/message. It is seven times slower than the fastest solution, but the luxury of matching the consumer's language does not come free.

    Other hand-coded solutions are fast too and you will not regret choosing any of them, except for the usage of regexp. Using regexp needs the shortest code snippet, but the performance is so tragic, that it is not worth it.

    My contribution to this topic, which you can try running in the playground:

    func formatInt(number int) string {
        output := strconv.Itoa(number)
        startOffset := 3
        if number < 0 {
            startOffset++
        }
        for outputIndex := len(output); outputIndex > startOffset; {
            outputIndex -= 3
            output = output[:outputIndex] + "," + output[outputIndex:]
        }
        return output
    }
    
    0 讨论(0)
  • 2020-12-04 21:59

    You can also use this small package: https://github.com/floscodes/golang-thousands.

    Just convert your number to a string an then use the Separate-function like this:

    n:="3478686" // your number as a string
    
    thousands.Separate(n, "en") // adds thousands separators. the second argument sets the language mode.
    
    0 讨论(0)
  • 2020-12-04 22:00

    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()
    }
    
    0 讨论(0)
提交回复
热议问题