【问题标题】:How to fmt.Printf an integer with thousands comma如何 fmt.Printf 带有千位逗号的整数
【发布时间】:2020-12-18 22:45:17
【问题描述】:

Go 的 fmt.Printf 是否支持输出带有千位逗号的数字?

fmt.Printf("%d", 1000) 输出1000,我可以指定什么格式来输出1,000

docs 似乎没有提到逗号,我无法立即在source 中看到任何内容。

【问题讨论】:

    标签: string go formatting printf


    【解决方案1】:

    使用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
    }
    

    【讨论】:

    • 这只是打印出来,还是有办法把它变成一个变量?
    • @zimdanen 如果要获取字符串,请使用Sprintf 而不是Printf,就像使用标准库一样。
    【解决方案2】:

    我写了a library for this 以及其他一些人类代表问题。

    示例结果:

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

    示例用法:

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

    【讨论】:

    • 嗯,您对“人”的定义似乎仅限于习惯于阅读英文数字的人。有关更广泛的定义,请参阅我自己使用 golang.org/x/text/message 的答案。
    【解决方案3】:

    没有一个 fmt 打印动词支持千位分隔符。

    【讨论】:

    • 改为使用golang.org/x/text/message。请参阅我自己的答案。
    • 它确实回答了这个问题,但根本没有帮助。 -1
    • @BoraM.Alper 它回答了这个问题,并帮助了我!
    • 我为my answer 中的早期答案编写了测试和基准测试。
    【解决方案4】:

    前言:我在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') 和可选的负号 ('-') 的数字,因此这些字符在 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 位数字组。完成后(不再有数字),输出字节切片将作为 string 返回。

    变化

    用递归处理否定

    如果您不太关心效率而更关心可读性,您可能会喜欢这个版本:

    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 位并附加一个逗号(',') 如果有更多数字,则分组符号。

    【讨论】:

    • 感谢您的详尽描述。 0 检测前导连字符的除法很可爱,也许太聪明了一半。我宁愿在辅助函数中引入一个显式分支,如commaCount 函数所示。我还提供了altCommaCount 来计算计数而不首先使用字符串转换,但在您的情况下,无论如何您都要创建字符串,所以不值得。 play.golang.org/p/NO5bAHs1lo
    • @sen 我同意,我还会在我的代码中添加一个if,目的是简短、紧凑和高效。
    • @seh 我修改了这个例子,去掉了“聪明”的部分,力求可读性。
    【解决方案5】:

    我在 Github 上发布了一个 Go sn-p over 一个函数,用于根据用户指定的千位分隔符、小数分隔符和小数精度呈现一个数字(float64 或 int)。

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

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

    【讨论】:

      【解决方案6】:

      这是一个函数,它接受一个整数和分组分隔符,并返回一个用指定分隔符分隔的字符串。我试图优化效率,在紧密循环中没有字符串连接或 mod/division。根据我的分析,它比我的 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()
      }
      

      【讨论】:

      【解决方案7】:

      这是一个使用正则表达式的简单函数:

      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

      【讨论】:

      • 这将分配和复制各种子字符串 ⌈(⌊log10 num⌋+1)/3⌉-1 次,更不用说扫描字符串到重复匹配正则表达式。
      • 它最多会运行 3 次,这对于几乎所有用例来说都可以忽略不计。
      • 三次?你怎么算的?另外,你怎么知道这么通用的所有这些用例? play.golang.org/p/MqbdnCkgQh
      • 每次调用编译正则表达式的成本也不容忽视。
      • 我认为正则表达式不会对很多人造成性能问题。如果这确实是一个问题,您可以将 MustCompile 从函数中移出,这样它只会出现一次。或者,如果您真的关心性能,请使用其他较低级别的更详细答案之一。
      【解决方案8】:

      我对早期答案中提供的解决方案的性能感兴趣,并写了tests with benchmarks for them,包括我的两个代码 sn-ps。以下结果是在 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's code snippet。虽然它是逐个数字而不是三位数一组,但它是最快的。
      • 如果你想要最短的合理代码sn-p,请查看mine below。它增加了最快解决方案一半以上的时间,但代码却缩短了三倍。
      • 如果您想要单行且不介意使用外部库,请选择github.com/dustin/go-humanize。它比最快的解决方案慢两倍以上,但该库可能会帮助您进行其他格式设置。
      • 如果您想要本地化输出,请选择golang.org/x/text/message。它比最快的解决方案慢七倍,但匹配消费者语言的奢侈并非免费。

      其他手动编码的解决方案也很快,您不会后悔选择其中任何一个,除了使用正则表达式。使用正则表达式需要最短的代码sn-p,但是性能太惨了,不值得。

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

      【讨论】:

        【解决方案9】:

        使用https://github.com/dustin/go-humanize .. 它有一堆帮手来处理这些事情。除了字节,如 MiB、MB 和其他好东西。

        【讨论】:

          【解决方案10】:

          这绝对不是基准测试的领导者,但谁在乎代码是否清晰且性能是否重要?

          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%

          【讨论】:

          【解决方案11】:

          humanize 包可以发挥作用!请参阅此软件包的文档here。要使用这个包,首先使用像 Git SCM 这样的工具安装它。如果您使用的是 Git Bash,请打开 shell 窗口并输入:

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

          完成后,您可以使用以下解决方案代码(例如,ma​​in.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)));
          }
          

          Commaf 还有其他变体,例如BigComma, Comma, BigCommaf 等,具体取决于您输入的数据类型。

          所以,在使用命令运行这个程序时:

          go run main.go
          

          您将看到如下输出

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

          【讨论】:

            【解决方案12】:

            你也可以使用这个小包:https://github.com/floscodes/golang-thousands

            只需将您的号码转换为字符串,然后像这样使用Separate-函数:

            n:="3478686" // your number as a string
            
            thousands.Separate(n, "en") // adds thousands separators. the second argument sets the language mode.
            

            【讨论】:

              【解决方案13】:

              如果您不想使用库(无论出于何种原因),我将其取消。它似乎可以工作并且可以使用任何指定的符文作为分隔符:

              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
              }
              

              注意:经过进一步测试,此功能无法完美运行。我建议使用@IvanTung 发布的解决方案,并欢迎任何可以让我完美工作的人进行任何编辑。

              【讨论】:

              • 这个算法坏了,当只传递三个数字时它显示“,123”,并且从未声明src
              • 你是对的,但这是一项匆忙的工作。我会建议@IvanTung 发布的解决方案。
              • strconv.Itoa 返回一个字符串,其中每个符文仅由一个字节组成。这对所有utf8.RunCountInString 来说都是矫枉过正。
              • @leylandski 你也可以删除你的答案,因为你意识到这是错误的。
              【解决方案14】:
              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]
              }
              

              【讨论】:

              • 哇,这肯定会产生很多垃圾字符串。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-12-06
              • 2010-09-11
              相关资源
              最近更新 更多