如何使用 fmt.Printf 在整数中添加千位分隔符

90

Go的fmt.Printf支持输出带千位分隔符的数字吗?

fmt.Printf("%d", 1000)会输出1000,我该使用什么格式来输出1,000呢?

文档中好像没有提到逗号,而且我在源代码中也没有找到相关内容。


逗号是美国的传统,但美国的标准是使用空格作为千位分隔符:https://physics.nist.gov/cuu/pdf/sp811.pdf#10.5.3 - undefined
14个回答

140
使用 golang.org/x/text/message 可以针对 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
}

1
这只是打印出来了吗?还是有一种方法可以将其存储到变量中? - zimdanen
1
@zimdanen 如果你想要获取字符串,就像使用标准库一样,请使用 Sprintf 而不是 Printf - Eagle
1
为什么我们需要在这里初始化东西?我以为这会像Sprintf一样,不需要分配内存吗? - TheRealChx101
1
@TheRealChx101,乍一看也有同样的疑问,但打印机的行为是可定制的,即使在实例化之后也可以进行调整。因此构造函数https://cs.opensource.google/go/x/text/+/refs/tags/v0.11.0:message/message.go;l=49-63;drc=9e2b64d659da1afe07ce1c9c1dfefc09d188f21e - thebearingedge
1
这个线程是安全的吗? - TheRealChx101

58

我也写了一份库,用于处理人类可读性等问题。

以下是一些示例结果:

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

使用示例:

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

19
你对“人类”的定义似乎只限于那些习惯使用英式数字的人。如果想要更广泛的定义,请看一下我自己的回答,它使用了“golang.org/x/text/message”。 - dolmen

32

当前的fmt印刷动词都不支持千位分隔符。


18
请使用golang.org/x/text/message代替。请参见我的答案。 - dolmen
24
确实回答了问题,但完全没有帮助。-1 - Bora M. Alper
1
@BoraM.Alper它回答了我的问题并帮助了我! - Trent
1
我在我的回答中编写了测试和基准。 - Ferdinand Prantl

23

前言:我在 github.com/icza/gox 上发布了一个带有更多自定义功能的实用程序,查看 fmtx.FormatInt()


fmt 包不支持分组小数。

我们必须自己实现(或使用现有的实现)。

代码

以下是一个紧凑而高效的解决方案(请参见解释):

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] = ','
        }
    }
}

测试一下:

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))
}

输出:

         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

说明:

基本上,Format() 函数的作用是对数字进行格式化而不使用分组,然后创建足够大的另一个片段,并在必要时插入逗号 (',') 分组符号来复制数字的位数(如果有更多的数字,则在每 3 个数字之后)。同时注意保留负号。

输出长度:

基本上,输出长度等于输入长度加上要插入的分组符号的数量。插入分组符号的数量为:

numOfCommas = (numOfDigits - 1) / 3

由于输入字符串是一个只包含数字('0..9')和可选负号('-')的数字,因此字符以 1 对 1 的方式映射到 UTF-8 编码中的字节中(这就是 Go 在内存中存储字符串的方式)。 因此,我们可以直接使用字节而不是符文。 因此,数字的位数是输入字符串的长度,如果数字为负,则可选地减去 1:

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循环,只需将数字的字节(数字)从输入字符串复制到输出,并在每组3个数字后插入分组符号(“,”),如果有更多数字。循环向下运行,因此更容易跟踪这些3个数字的组。一旦完成(没有更多数字),输出字节切片将作为字符串返回。
变体
使用递归处理负数
如果您更关心可读性而不是效率,则可能会喜欢这个版本:
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] = ','
        }
    }
}

基本上这个函数通过递归调用处理负数:如果这个数字是负数,它会使用绝对值递归调用自己并在结果前加上一个"-"字符串。

使用 append() 和切片

以下是另一个版本,使用内置的append()函数和切片操作。相对容易理解但性能不如上一个版本:

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)
}

第一个if语句处理第一个可选的、不完整的组,如果存在并且少于3个数字,则后续的for循环将处理其余部分,每次迭代复制3个数字并添加逗号(',')分组符号。


2
感谢您提供详细的描述。通过除以 0 来检测前导连字符是巧妙的,但也许过于聪明了一半。我更喜欢在辅助函数中引入一个显式分支,就像 commaCount 函数所示。我还提供了 altCommaCount 来计算不使用字符串转换的计数,但在您的情况下,您无论如何都要创建字符串,所以这并不值得。https://play.golang.org/p/NO5bAHs1lo - seh
@sen 我同意,我也会在我的代码中添加一个 if,这里的目的是让代码变得简短、紧凑和高效。 - icza
1
@seh 我重新编写了这个例子,并去掉了“聪明”的部分,力求可读性。 - icza

16

我在Github上发布了一个Go代码片段,用于根据用户指定的千位分隔符、小数点分隔符和小数精度呈现数字(float64或int)。

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

用法: s := RenderFloat(format, n)
format参数告诉如何呈现数字n。
格式字符串示例,给定n = 12345.6789:
"#,###.##" => "12,345.67" "#,###." => "12,345" "#,###" => "12345,678" "#\u202F###,##" => "12 345,67" "#.###,###### => 12.345,678900 "" (默认格式) => 12,345.67

10
这里是一个使用正则表达式的简单函数:
import (
    "regexp"
)

func formatCommas(num int) string {
    str := fmt.Sprintf("%d", num)
    re := regexp.MustCompile("(\\d+)(\\d{3})")
    for n := ""; n != str; {
        n = str
        str = re.ReplaceAllString(str, "$1,$2")
    }
    return str
}

例子:

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

输出:

1,000
-1,000,000,000

https://play.golang.org/p/vnsAV23nUXv


1
这将会分配并复制各种子字符串 ⌈(⌊log10 num⌋+1)/3⌉-1 次,更不用说重复扫描字符串以匹配正则表达式的成本了。 - seh
它最多运行3次,对于几乎所有的使用情况来说都应该是可以忽略不计的。 - jchavannes
1
三次?你怎么算的?另外,你怎么知道这么一般的东西所有的用例?https://play.golang.org/p/MqbdnCkgQh - seh
每次调用编译正则表达式的成本也绝不可忽视。 - dolmen
我认为正则表达式不会对很多人造成性能问题。如果这真的是个问题,你可以将MustCompile移出函数,这样它只会发生一次。或者,如果你真的关心性能,可以使用其他更低级、更冗长的答案之一。 - jchavannes

10
下面是一个函数,它接受一个整数和分组分隔符,并返回用指定分隔符分隔的字符串。我试图优化效率,在紧密循环中没有使用字符串连接或模数/除法。从我的分析来看,在我的 Mac 上,它比 humanize.Commas 实现(~680ns vs 1642ns)快两倍以上。我是 Go 的新手,希望看到更快的实现!
使用方法:s := NumberToString(n int, sep rune) 示例 演示使用不同的分隔符(',' vs ' '),并验证 int 值范围。
s:= NumberToString(12345678, ',')
=> "12,345,678"
s:= NumberToString(12345678, ' ')
=> "12 345 678"
s: = NumberToString(-9223372036854775807, ',')
=> "-9,223,372,036,854,775,807" 函数实现
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()
}

这是您的函数的重写版本,它使用[]byte而不是bytes.Buffer:https://play.golang.org/p/fkg7FsquII - dolmen

8

我对早期答案提供的解决方案的性能感到兴趣,并编写了带有基准测试的测试,其中包括我的两个代码片段。以下结果是在 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 |
+---------------------+-------------------------------------------+--------------+
  • 如果您想要最快的解决方案,请使用@icza的代码片段。虽然它逐位处理数字而不是将数字分组为三个一组,但它被证明是最快的。
  • 如果您想要最短的代码片段,请看下面的我的示例。它增加了超过最快解决方案的一半时间,但代码长度缩短了三倍。
  • 如果您想要一个单行的解决方案,并且不介意使用外部库,请选择github.com/dustin/go-humanize。它比最快的解决方案慢了两倍以上,但该库可能会帮助您进行其他格式化。
  • 如果您想要本地化输出,请选择golang.org/x/text/message。它比最快的解决方案慢了七倍,但与消费者语言匹配的奢侈品并非免费。

其他手写的解决方案也很快,您不会后悔选择任何一个,除了使用regexp。 使用regexp需要长度最短的代码片段,但性能非常差,不值得。

我对这个话题的贡献,您可以在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
}

3
这绝对不是主导基准测试的性能,但如果代码清晰,而且性能并不重要,谁会在意呢?
package main
import (
    "fmt"
)

func IntComma(i int) string {
    if (i < 0) {
        return "-" + IntComma(-i)
    }
    if (i < 1000) {
        return fmt.Sprintf("%d",i)
    }
    return IntComma(i / 1000) + "," + fmt.Sprintf("%03d",i % 1000)
}

func main() {
    fmt.Println(IntComma(1234567891234567))
}

这是关于基准测试的内容:实现与icza非常相似。

func IntCommaB(num int) string {
        str := strconv.Itoa(num)
        l_str := len(str)
        digits := l_str
        if num < 0 {
                digits--
        }
        commas := (digits + 2) / 3 - 1
        l_buf := l_str + commas 
        var sbuf [32]byte // pre allocate buffer at stack rather than make([]byte,n)
        buf := sbuf[0:l_buf]
        // copy str from the end
        for s_i, b_i, c3 := l_str-1, l_buf-1, 0; ;  {
                buf[b_i] = str[s_i]
                if s_i == 0 {
                        return string(buf)
                }
                s_i--
                b_i--
                // insert comma every 3 chars
                c3++
                if c3 == 3 && (s_i > 0 || num>0)  {
                        buf[b_i] = ','
                        b_i--
                        c3 = 0
                }
    }
}

使用输入数字-1234567890123456789时,它比icza的方案约快15%。


如果您想要清晰的代码且性能不是关键问题,只需使用一个经过验证的库,例如golang.org/x/text/message - dolmen

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接