Golang 对金融数字的格式化

前言

前几天心血来潮,想把之前用 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, 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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!