<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          四舍五入在 Go 語(yǔ)言中為何如此困難?

          共 9666字,需瀏覽 20分鐘

           ·

          2021-08-20 20:13

          四舍五入是一個(gè)非常常見(jiàn)的功能,在流行語(yǔ)言標(biāo)準(zhǔn)庫(kù)中往往存在 Round 的功能,它最少支持常用的 Round half up 算法。

          而在 Go 語(yǔ)言中這似乎成為了難題,在 stackoverflow 上搜索 [go] Round 會(huì)存在大量相關(guān)提問(wèn),Go 1.10 開(kāi)始才出現(xiàn) math.Round 的身影,本以為 Round 的疑問(wèn)就此結(jié)束,但是一看函數(shù)注釋 Round returns the nearest integer, rounding half away from zero ,這是并不常用的 Round half away from zero 實(shí)現(xiàn)呀,說(shuō)白了就是我們理解的 Round 閹割版,精度為 0 的 Round half up 實(shí)現(xiàn),Round half away from zero 的存在是為了提供一種高效的通過(guò)二進(jìn)制方法得結(jié)果,可以作為 Round 精度為 0 時(shí)的高效實(shí)現(xiàn)分支。

          帶著對(duì) Round 的‘敬畏’,我在 stackoverflow 翻閱大量關(guān)于 Round 問(wèn)題,開(kāi)啟尋求最佳的答案,本文整理我認(rèn)為有用的實(shí)現(xiàn),簡(jiǎn)單分析它們的優(yōu)缺點(diǎn),對(duì)于不想逐步了解,想直接看結(jié)果的小伙伴,可以直接看文末的最佳實(shí)現(xiàn),或者跳轉(zhuǎn) exmath.Round[1] 直接看源碼和使用吧!

          Round 第一彈

          stackoverflow[2] 問(wèn)題中的最佳答案首先獲得我的關(guān)注,它在 mathx.Round[3] 被開(kāi)源,以下是代碼實(shí)現(xiàn):
          // source: https://github.com/icza/gox/blob/master/mathx/mathx.go
          package mathx

          import "math"

          // AbsInt returns the absolute value of i.
          func AbsInt(i int) int {
           if i < 0 {
            return -i
           }
           return i
          }

          // Round returns x rounded to the given unit.
          // Tip: x is "arbitrary", maybe greater than 1.
          // For example:
          //     Round(0.363636, 0.001) // 0.364
          //     Round(0.363636, 0.01)  // 0.36
          //     Round(0.363636, 0.1)   // 0.4
          //     Round(0.363636, 0.05)  // 0.35
          //     Round(3.2, 1)          // 3
          //     Round(32, 5)           // 30
          //     Round(33, 5)           // 35
          //     Round(32, 10)          // 30
          //
          // For details, see https://stackoverflow.com/a/39544897/1705598
          func Round(x, unit float64) float64 {
           return math.Round(x/unit) * unit
          }

          // Near reports if 2 float64 numbers are "near" to each other.
          // The caller is responsible to provide a sensible epsilon.
          //
          // "near" is defined as the following:
          //     near := math.Abs(a - b) < eps
          //
          // Corner cases:
          //  1. if a==b, result is true (eps will not be checked, may be NaN)
          //  2. Inf is near to Inf (even if eps=NaN; consequence of 1.)
          //  3. -Inf is near to -Inf (even if eps=NaN; consequence of 1.)
          //  4. NaN is not near to anything (not even to NaN)
          //  5. eps=Inf results in true (unless any of a or b is NaN)
          func Near(a, b, eps float64) bool {
           // Quick check, also handles infinities:
           if a == b {
            return true
           }

           return math.Abs(a-b) < eps
          }

          這個(gè)實(shí)現(xiàn)非常的簡(jiǎn)潔,借用了 math.Round,由此看來(lái) math.Round 還是很有價(jià)值的,大致測(cè)試了它的性能一次運(yùn)算大概 0.4ns,這非常的快。

          但是我也很快發(fā)現(xiàn)了它的問(wèn)題,就是精度問(wèn)題,這個(gè)是問(wèn)題中一個(gè)回答的解釋讓我有了警覺(jué),并開(kāi)始了實(shí)驗(yàn)。他認(rèn)為使用浮點(diǎn)數(shù)確定精度(mathx.Round 的第二個(gè)參數(shù))是不恰當(dāng)?shù)?,因?yàn)楦↑c(diǎn)數(shù)本身并不精確,例如 0.05 在 64 位 IEEE 浮點(diǎn)數(shù)中,可能會(huì)將其存儲(chǔ)為 0.05000000000000000277555756156289135105907917022705078125。

          //source: https://play.golang.org/p/0uN1kEG30kI
          package main

          import (
           "fmt"
           "math"
          )

          func main() {
           f := 12.15807659924030304
           fmt.Println(Round(f, 0.0001)) // 12.158100000000001

           f = 0.15807659924030304
           fmt.Println(Round(f, 0.0001)) // 0.15810000000000002
          }

          func Round(x, unit float64) float64 {
           return math.Round(x/unit) * unit
          }

          以上代碼可以在 Go Playground[4] 上運(yùn)行,得到結(jié)果并非如期望那般,這個(gè)問(wèn)題主要出現(xiàn)在 math.Round(x/unit) 與 unit 運(yùn)算時(shí),math.Round 運(yùn)算后一定會(huì)是一個(gè)精確的整數(shù),但是 0.0001 的精度存在誤差,所以導(dǎo)致最終得到的結(jié)果精度出現(xiàn)了偏差。

          格式化與反解析

          在這個(gè)問(wèn)題中也有人提出了先用 fmt.Sprintf 對(duì)結(jié)果進(jìn)行格式化,然后再采用 strconv.ParseFloat 反向解析,Go Playground[5] 代碼在這個(gè)里。

          source: https://play.golang.org/p/jxILFBYBEF
          package main

          import (
           "fmt"
           "strconv"
          )

          func main() {
           fmt.Println(Round(0.3636360.05)) // 0.35
           fmt.Println(Round(3.2320.05))    // 3.25
           fmt.Println(Round(0.48880.05))   // 0.5
          }

          func Round(x, unit float64) float64 {
           var rounded float64
           if x > 0 {
            rounded = float64(int64(x/unit+0.5)) * unit
           } else {
            rounded = float64(int64(x/unit-0.5)) * unit
           }
           formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)
           if err != nil {
            return rounded
           }
           return formatted
          }

          這段代碼中有點(diǎn)問(wèn)題,第一是結(jié)果不對(duì),和我們理解的存在差異,后來(lái)一看第二個(gè)參數(shù)傳錯(cuò)了,應(yīng)該是 0.01,我想試著調(diào)整調(diào)整精度吧,我改成了 0.0001 之后發(fā)現(xiàn)一直都是保持小數(shù)點(diǎn)后兩位,我細(xì)細(xì)研究了下這段代碼的邏輯,發(fā)現(xiàn) fmt.Sprintf("%.2f", rounded) 中寫(xiě)死了保留的位數(shù),所以它并不通用,我嘗試如下簡(jiǎn)單調(diào)整一下使其生效。

          package main

          import (
           "fmt"
           "strconv"
          )

          func main() {
           f := 12.15807659924030304
           fmt.Println(Round(f, 0.0001)) // 12.1581

           f = 0.15807659924030304
           fmt.Println(Round(f, 0.0001)) // 0.1581

           fmt.Println(Round(0.3636360.0001)) // 0.3636
           fmt.Println(Round(3.2320.0001))    // 3.232
           fmt.Println(Round(0.48880.0001))   // 0.4888
          }

          func Round(x, unit float64) float64 {
           var rounded float64
           if x > 0 {
            rounded = float64(int64(x/unit+0.5)) * unit
           } else {
            rounded = float64(int64(x/unit-0.5)) * unit
           }

           var precision int
           for unit < 1 {
            precision++
            unit *= 10
           }

           formatted, err := strconv.ParseFloat(fmt.Sprintf("%."+strconv.Itoa(precision)+"f", rounded), 64)
           if err != nil {
            return rounded
           }
           return formatted
          }

          確實(shí)獲得了滿意的精準(zhǔn)度,但是其性能也非??陀^,達(dá)到了 215ns/op,暫時(shí)看來(lái)如果追求精度,這個(gè)算法目前是比較完美的。

          大道至簡(jiǎn)

          很快我發(fā)現(xiàn)了另一個(gè)極簡(jiǎn)的算法,它的精度和速度都非常的高,實(shí)現(xiàn)還特別精簡(jiǎn):

          package main

          import (
           "fmt"
           "github.com/thinkeridea/go-extend/exmath"
          )

          func main() {
           f := 0.15807659924030304
           fmt.Println(float64(int64(f*10000+0.5)) / 10000// 0.1581
          }
          這并不通用,除非像以下這么包裝:
          func Round(x, unit float64) float64 {
           return float64(int64(x*unit+0.5)) / unit
          }

          unit 參數(shù)和之前的概念不同了,保留一位小數(shù) uint =10,只是整數(shù) uint=1, 想對(duì)整數(shù)部分進(jìn)行精度控制 uint=0.01 例如:Round(1555.15807659924030304, 0.01) = 1600,Round(1555.15807659924030304, 1) = 1555,Round(1555.15807659924030304, 10000) = 1555.1581。

          這似乎就是終極答案了吧,等等……

          終極方案

          上面的方法夠簡(jiǎn)單,也夠高效,但是 api 不太友好,第二個(gè)參數(shù)不夠直觀,帶了一定的心智負(fù)擔(dān),其它語(yǔ)言都是傳遞保留多少位小數(shù),例如 Round(1555.15807659924030304, 0) = 1555,Round(1555.15807659924030304, 2) = 1555.16,Round(1555.15807659924030304, -2) = 1600,這樣的交互才符合人性啊。

          別急我在 go-extend[6] 開(kāi)源了 exmath.Round[7],其算法符合通用語(yǔ)言 Round 實(shí)現(xiàn),且遵循 Round half up 算法要求,其性能方面在 3.50ns/op, 具體可以參看調(diào)優(yōu) exmath.Round 算法[8], 具體代碼如下:

          //source: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go

          // MIT License
          // Copyright (c) 2020 Qi Yin <[email protected]>

          package exmath

          import (
           "math"
          )

          // Round 四舍五入,ROUND_HALF_UP 模式實(shí)現(xiàn)
          // 返回將 val 根據(jù)指定精度 precision(十進(jìn)制小數(shù)點(diǎn)后數(shù)字的數(shù)目)進(jìn)行四舍五入的結(jié)果。precision 也可以是負(fù)數(shù)或零。
          func Round(val float64, precision int) float64 {
           if precision == 0 {
            return math.Round(val)
           }

           p := math.Pow10(precision)
           if precision < 0 {
            return math.Floor(val*p+0.5) * math.Pow10(-precision)
           }

           return math.Floor(val*p+0.5) / p
          }

          總結(jié)

          Round 功能雖簡(jiǎn)單,但是受到 float 精度影響,仍然有很多人在四處尋找穩(wěn)定高效的算法,參閱了大多數(shù)資料后精簡(jiǎn)出 exmath.Round[9] 方法,期望對(duì)其他開(kāi)發(fā)者有所幫助,至于其精度使用了大量的測(cè)試用例,沒(méi)有超過(guò) float 精度范圍時(shí)并沒(méi)有出現(xiàn)精度問(wèn)題,未知問(wèn)題等待社區(qū)檢驗(yàn),具體測(cè)試用例參見(jiàn) round_test[10]。

          原文鏈接:https://blog.thinkeridea.com/202101/go/round.html

          作者:戚銀

          參考資料

          [1]

          exmath.Round: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go#L12

          [2]

          stackoverflow: https://stackoverflow.com/questions/39544571/golang-round-to-nearest-0-05

          [3]

          mathx.Round: https://github.com/icza/gox/blob/master/mathx/mathx.go#L26

          [4]

          Go Playground: https://play.golang.org/p/0uN1kEG30kI

          [5]

          Go Playground: https://play.golang.org/p/jxILFBYBEF

          [6]

          go-extend: https://github.com/thinkeridea/go-extend

          [7]

          exmath.Round: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go

          [8]

          調(diào)優(yōu) exmath.Round 算法: https://github.com/thinkeridea/go-extend/pull/13

          [9]

          exmath.Round: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go

          [10]

          round_test: https://github.com/thinkeridea/go-extend/blob/main/exmath/round_test.go


          推薦閱讀

          一個(gè) Go 語(yǔ)言實(shí)現(xiàn)的數(shù)據(jù)庫(kù)

          推薦一本免費(fèi)的 Go 書(shū)籍!

          Go 語(yǔ)言將成為惡意軟件開(kāi)發(fā)者的首選

          Go語(yǔ)言基礎(chǔ)編程學(xué)習(xí)資料一套

          這本Go新書(shū),可以免費(fèi)下載了

          瀏覽 61
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产免费观看黄色电影 | 五月激情丁香 | 天天做天天爱天天综合网 | 久操视频免费看 | 婷操|