Golang 對金融數字的格式化

root01發表於2020-06-25

前言

前幾天心血來潮,想把之前用 Python 寫的“金融數字格式化”程式碼(金融數值格式化:從右往左,每隔 3 位,加一個逗號分隔,看起來清晰),重新用 Golang 寫。
寫了 2 天,終於完工了。獻醜出來,懇請各位大佬指點高效的解決方案。


整體思路

假設所有的金融數值都是以字串儲存的。(暫時不考慮如何將數值轉換為字串,數值大的時候連 int64/float64 都能輕鬆溢位,需要使用 math/big 包下,像 big.NewInt() 這樣的型別來儲存大數值)。
假如說是 Web 前端傳到 Go 後端的值,那就更簡單了。AJAX 過來的值,都是字串,根本不需要再考慮用哪個型別去儲存數值。

實現方式

暴力遍歷法。儘可能多地考慮到現實情況(永遠不要相信使用者的輸入,萬一使用者亂輸入內容呢?)

1.清洗

將這個字串進行清洗,清除掉那些無意義的字元。比如:數值前面有多餘的正/負號 (-+123456)、多餘的 0 (0000123456),小數值後面多餘的 0 (123456.1230000)。

2.檢查符號

清洗完無意義符號後,檢查這個字串是否帶有符號,有符號則提取出這個符號,到最後用於拼接。

3.校驗是否為正常的數值

已經清洗完了多餘的字元,進行校驗,判斷這個字串是否為合法的數值。數值中最多隻能有一個小數點.、不能出現非數值字元(不考慮科學計數以及 complex 型別)。

4.針對浮點數值的小數部分操作

如果這個字串是浮點數值,把小數部分,連帶小數點全部提取出來,到最後用於拼接。

5.執行核心功能

每隔 3 位,加一個單字元的逗號,。(程式碼來源:《Goc程式設計語言》第 3.5.4, P54, gopl.io/ch3/comma)

6.最終的拼接

按順序將:正/負號、分隔完成的字串、小數部分,拼接成一個最終的完整字串。


我的實現程式碼

package main

import (
    "fmt"
    "strings"
    "unicode"
)

//校驗這個字串是不是合法的數值
func validateDigit(s string) bool {
    dotCount := 0 //統計小數點有幾個,只能出現一個小數點

    for _, v := range s {
        if v == '.' {
            dotCount++
            if dotCount > 1 { //只允許有一個小數點存在
                return false
            }
            continue
        }

        if !unicode.IsDigit(v) {
            return false
        }
    }

    return true
}

//清除多餘字元的函式
func cleanUnneededChar(price string) string {
    //統計頂頭是否有多餘的正/負號,頂頭只允許一個正/負號
    symbolI, symbolJ := 0, 1
    for {
        if symbolJ == len(price)-1 {
            break
        }

        if price[symbolI] == '-' || price[symbolI] == '+' {
            if price[symbolJ] == '-' || price[symbolJ] == '+' {
                symbolI++
                symbolJ++
            } else {
                break
            }
        } else {
            break
        }
    }

    price = price[symbolI:] //裁剪掉頂頭多餘的正/負號

    //檢查這個字串是否帶有一個正/負號。如果帶有符號,就把符號先單獨提取出來
    symbolString := ""
    if price[0] == '-' || price[0] == '+' {
        symbolString = string(price[0])
        price = price[1:]
    }

    if len(price) == 1 {
        return symbolString + price
    }

    //統計頂頭有多少個多餘的 0
    zeroI := 0
    for {
        if price[zeroI] != '0' {
            break
        } else {
            zeroI++
        }
    }

    //如果這個數值不是浮點數,那麼後面的0都是不允許動的
    dotIndex := strings.Index(price, ".")
    if dotIndex == -1 {
        if zeroI > 0 { //zeroI 是統計頂頭是否有多餘的 0,如果這個值大於了 0,說明頂頭有多餘的 0
            return symbolString + price[zeroI:] //裁剪掉頂頭多餘的 0,然後帶上符號並返回
        }

        return symbolString + price //頂頭沒有多餘的 0,沒必要裁剪了
    }

    //程式碼能走到這裡說明這個肯定是浮點數了,至少是帶有小數點了
    //查詢一下小數點後面有沒有字元。如果沒有字元,說明只有孤零零的一個小數點字元,那就把這個多餘的小數點清除掉
    if price[dotIndex:] == "." {
        price = price[zeroI:dotIndex]
    }

    //只有為浮點數值了,才清除掉末尾多餘的0
    end := len(price) //最末的下標,往前推
    for {
        if price[end-1] != '0' {
            if price[end-1] == '.' { //判斷一下末尾是不是還有多餘的 '.'
                end--
            }
            break
        } else {
            end--
        }
    }

    return symbolString + price[zeroI:end]
}

//核心功能:從右向左,每隔 3 位,加一個單字元的逗號 ','
func comma(s string) string {
    if len(s) <= 3 {
        return s
    }

    return comma(s[:len(s)-3]) + "," + comma(s[len(s)-3:])
}

//將一個數值字串按金融化輸出(每隔 3 位加一個逗號)
/*
思路:暴力遍歷的方式
1.清理掉字串中多餘的正/負號、多餘的0
2.如果這個字串帶有符號,則提取出這個符號,到最後用於拼接
3.已經清洗完了多餘的字元,進行校驗,判斷這個字串是否為合法的數值
4.如果這個字串是浮點數值,把小數部分,連帶小數點全部提取出來,到最後用於拼接
5.執行核心功能,每隔 3 位,加一個單字元的逗號 ','
6.按順序將:正/負號、分隔完成的字串、小數部分,拼接成一個最終的完整字串
*/
func FormatFinancialString(price string) string {
    //清理掉多餘的字元。比如:浮點數末尾的0、開頭的0、多餘的正/負號
    price = cleanUnneededChar(price)

    //檢查這個字串是否帶有正/負號。如果帶有符號,就把符號先單獨提取出來
    symbolString := ""
    if price[0] == '-' || price[0] == '+' {
        symbolString = string(price[0])
        price = price[1:]
    }

    //清洗完了多餘字元,開始校驗這個數值字串
    if !validateDigit(price) {
        return "非法的數值!請檢查您提供的數值是否正確!數值允許是浮點數,數字的前面一位允許帶有一個正/負號!"
    }

    //小數點前沒有寫0,就補一個0進去補齊,讓數字字串看起來更好看
    if price[0] == '.' {
        return "0" + price
    }

    //判斷這個數字是不是浮點數值
    dotIndex, decimalString := strings.Index(price, "."), ""
    if dotIndex != -1 {
        decimalString = price[dotIndex:]
        price = price[:dotIndex]
    } else if dotIndex == -1 {
        dotIndex = len(price)
    }

    return fmt.Sprintf("%s%s%s", symbolString, comma(price[:dotIndex]), decimalString)
}

func main() {
    fmt.Println(FormatFinancialString("123456789"))
    fmt.Println(FormatFinancialString("-123456789"))
    fmt.Println(FormatFinancialString("+123456789"))

    fmt.Println("------------- 測試一些頂頭有多餘符號的字串 -------------")

    fmt.Println(FormatFinancialString("-++111222333"))
    fmt.Println(FormatFinancialString("+-+-111222333"))
    fmt.Println(FormatFinancialString("++-"))
    fmt.Println(FormatFinancialString("++1"))
    fmt.Println(FormatFinancialString("--1"))
    fmt.Println(FormatFinancialString("+++++++2222"))
    fmt.Println(FormatFinancialString("-------33333"))

    fmt.Println("----------------------------")

    fmt.Println(FormatFinancialString("12345678987654.12345"))
    fmt.Println(FormatFinancialString("-12345678912345.12345"))
    fmt.Println(FormatFinancialString("+12345678912345.12345"))

    fmt.Println("------------- 混合測試 -------------")

    fmt.Println(FormatFinancialString("0000001"))
    fmt.Println(FormatFinancialString("-++-+----0000001"))
    fmt.Println(FormatFinancialString("+---++0000001"))
    fmt.Println(FormatFinancialString("00001597530"))
    fmt.Println(FormatFinancialString(".789456123"))
    fmt.Println(FormatFinancialString("321654987."))
    fmt.Println(FormatFinancialString("1234567.0000000"))
    fmt.Println(FormatFinancialString("-1234567.1530000"))
    fmt.Println(FormatFinancialString("-++000000067.00001"))

    fmt.Println("------------- 測試一些含有非法字元的字串 -------------")

    fmt.Println(FormatFinancialString("+1234.567.12345"))
    fmt.Println(FormatFinancialString("a1234567.12345"))
    fmt.Println(FormatFinancialString("+123a4567.12345"))
    fmt.Println(FormatFinancialString("+12304567.123a45"))
    fmt.Println(FormatFinancialString("12304567.123a45"))
    fmt.Println(FormatFinancialString("中12304567.12345"))
}



執行效果

Golang 對金融數字的格式化


帶貨幣符號的處理

程式碼中沒有考慮到帶貨幣符號的情況,後來在網上看了現成的輪子示例時,才意識到這個問題。
遇到帶貨幣符號的情況,我個人的思路:把貨幣符號的字串與格式化後的字串拼接在一起,貨幣符號的字串放在最前面。


現成輪子

網上找到了這個現成的輪子:accounting - money and currency formatting for golang

本作品採用《CC 協議》,轉載必須註明作者和本文連結

I'm a rookie gopher, a pythonic gopher.

相關文章