123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- package ut
- import (
- "errors"
- "fmt"
- "log"
- "math"
- "regexp"
- "strconv"
- "strings"
- "sync"
- )
- // numberFormat is a struct that contains all the information about number
- // formatting for a specific locale that we need to do number, currency, and
- // percentage formatting
- type numberFormat struct {
- positivePrefix string
- positiveSuffix string
- negativePrefix string
- negativeSuffix string
- multiplier int
- minDecimalDigits int
- maxDecimalDigits int
- minIntegerDigits int
- groupSizeFinal int // only the right-most (least significant) group
- groupSizeMain int // all other groups
- }
- // CurrencyType is the type of Currency your converting
- type CurrencyType int
- // Currency Types such as Standard vs Accounting notation i.e. -$123.50 vs ($123.50)
- const (
- CurrencyStandard CurrencyType = iota
- CurrencyAccounting
- )
- var (
- // numberFormats keeps a copy of all numberFormat instances that have been
- // loaded before, to prevent parsing a single number format string multiple
- // times. There is vey little danger of this list consuming too much memory,
- // since the data for each of these is pretty small in size, and the same
- // formats are used by multiple locales.
- numberFormats = map[string]*numberFormat{}
- numberFormatsNoDecimals = map[string]*numberFormat{}
- nfMutex = new(sync.RWMutex)
- nfndMutex = new(sync.RWMutex)
- // prefixSuffixRegex is a regular expression that is used to parse number
- // formats
- prefixSuffixRegex = regexp.MustCompile(`(.*?)[#,\.0]+(.*)`)
- )
- func getCurrencyPattern(typ CurrencyType, nf NumberFormats) string {
- if typ == CurrencyStandard {
- return nf.Currency
- }
- return nf.CurrencyAccounting
- }
- // FmtCurrency takes a float number and a currency key and returns a string
- // with a properly formatted currency amount with the correct currency symbol.
- // If a symbol cannot be found for the reqested currency, this will return blank,
- // use FmtCurrencySafe for variant.
- func (n Number) FmtCurrency(typ CurrencyType, currency string, number float64) string {
- formatted, err := n.FmtCurrencySafe(typ, currency, number)
- if err != nil {
- fmt.Println(err)
- }
- return formatted
- }
- // FmtCurrencySafe takes a float number and a currency key and returns a string
- // with a properly formatted currency amount with the correct currency symbol.
- // If a symbol cannot be found for the reqested currency, the the key is used
- // instead. If the currency key requested is not recognized, it is used as the
- // symbol, and an error is returned with the formatted string.
- func (n Number) FmtCurrencySafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {
- format := n.parseFormat(getCurrencyPattern(typ, n.Formats), true)
- result := n.formatNumber(format, number)
- c, ok := n.Currencies[currency]
- if !ok {
- s := "**** WARNING **** unknown currency: " + currency
- err = errors.New(s)
- log.Println(s)
- formatted = strings.Replace(result, "¤", currency, -1)
- return
- }
- formatted = strings.Replace(result, "¤", c.Symbol, -1)
- return
- }
- // FmtCurrencyWhole does exactly what FormatCurrency does, but it leaves off
- // any decimal places. AKA, it would return $100 rather than $100.00.
- // If a symbol cannot be found for the reqested currency, this will panic, use
- // FmtCurrencyWholeSafe for non panicing variant.
- func (n Number) FmtCurrencyWhole(typ CurrencyType, currency string, number float64) string {
- formatted, err := n.FmtCurrencyWholeSafe(typ, currency, number)
- if err != nil {
- fmt.Println(err)
- }
- return formatted
- }
- // FmtCurrencyWholeSafe does exactly what FormatCurrency does, but it leaves off
- // any decimal places. AKA, it would return $100 rather than $100.00.
- func (n Number) FmtCurrencyWholeSafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {
- format := n.parseFormat(getCurrencyPattern(typ, n.Formats), false)
- result := n.formatNumber(format, number)
- c, ok := n.Currencies[currency]
- if !ok {
- s := "**** WARNING **** unknown currency: " + currency
- err = errors.New(s)
- log.Println(s)
- formatted = strings.Replace(result, "¤", currency, -1)
- return
- }
- formatted = strings.Replace(result, "¤", c.Symbol, -1)
- return
- }
- // FmtNumber takes a float number and returns a properly formatted string
- // representation of that number according to the locale's number format.
- func (n Number) FmtNumber(number float64) string {
- return n.formatNumber(n.parseFormat(n.Formats.Decimal, true), number)
- }
- // FmtNumberWhole does exactly what FormatNumber does, but it leaves off any
- // decimal places. AKA, it would return 100 rather than 100.01.
- func (n Number) FmtNumberWhole(number float64) string {
- return n.formatNumber(n.parseFormat(n.Formats.Decimal, false), number)
- }
- // FmtPercent takes a float number and returns a properly formatted string
- // representation of that number as a percentage according to the locale's
- // percentage format.
- func (n Number) FmtPercent(number float64) string {
- return n.formatNumber(n.parseFormat(n.Formats.Percent, true), number)
- }
- // parseFormat takes a format string and returns a numberFormat instance
- func (n Number) parseFormat(pattern string, includeDecimalDigits bool) *numberFormat {
- if includeDecimalDigits {
- nfMutex.RLock()
- if format, exists := numberFormats[pattern]; exists {
- nfMutex.RUnlock()
- return format
- }
- nfMutex.RUnlock()
- } else {
- nfndMutex.RLock()
- if format, exists := numberFormatsNoDecimals[pattern]; exists {
- nfndMutex.RUnlock()
- return format
- }
- nfndMutex.RUnlock()
- }
- format := new(numberFormat)
- patterns := strings.Split(pattern, ";")
- matches := prefixSuffixRegex.FindAllStringSubmatch(patterns[0], -1)
- if len(matches) > 0 {
- if len(matches[0]) > 1 {
- format.positivePrefix = matches[0][1]
- }
- if len(matches[0]) > 2 {
- format.positiveSuffix = matches[0][2]
- }
- }
- // default values for negative prefix & suffix
- format.negativePrefix = string(n.Symbols.Negative) + string(format.positivePrefix)
- format.negativeSuffix = format.positiveSuffix
- // see if they are in the pattern
- if len(patterns) > 1 {
- matches = prefixSuffixRegex.FindAllStringSubmatch(patterns[1], -1)
- if len(matches) > 0 {
- if len(matches[0]) > 1 {
- format.negativePrefix = matches[0][1]
- }
- if len(matches[0]) > 2 {
- format.negativeSuffix = matches[0][2]
- }
- }
- }
- pat := patterns[0]
- if strings.Index(pat, "%") != -1 {
- format.multiplier = 100
- } else if strings.Index(pat, "‰") != -1 {
- format.multiplier = 1000
- } else {
- format.multiplier = 1
- }
- pos := strings.Index(pat, ".")
- if pos != -1 {
- pos2 := strings.LastIndex(pat, "0")
- if pos2 > pos {
- format.minDecimalDigits = pos2 - pos
- }
- pos3 := strings.LastIndex(pat, "#")
- if pos3 >= pos2 {
- format.maxDecimalDigits = pos3 - pos
- } else {
- format.maxDecimalDigits = format.minDecimalDigits
- }
- pat = pat[0:pos]
- }
- p := strings.Replace(pat, ",", "", -1)
- pos = strings.Index(p, "0")
- if pos != -1 {
- format.minIntegerDigits = strings.LastIndex(p, "0") - pos + 1
- }
- p = strings.Replace(pat, "#", "0", -1)
- pos = strings.LastIndex(pat, ",")
- if pos != -1 {
- format.groupSizeFinal = strings.LastIndex(p, "0") - pos
- pos2 := strings.LastIndex(p[0:pos], ",")
- if pos2 != -1 {
- format.groupSizeMain = pos - pos2 - 1
- } else {
- format.groupSizeMain = format.groupSizeFinal
- }
- }
- if includeDecimalDigits {
- nfMutex.Lock()
- numberFormats[pattern] = format
- nfMutex.Unlock()
- return format
- }
- format.maxDecimalDigits = 0
- format.minDecimalDigits = 0
- nfndMutex.Lock()
- numberFormatsNoDecimals[pattern] = format
- nfndMutex.Unlock()
- return format
- }
- // formatNumber takes an arbitrary numberFormat and a number and applies that
- // format to that number, returning the resulting string
- func (n Number) formatNumber(format *numberFormat, number float64) string {
- negative := number < 0
- // apply the multiplier first - this is mainly used for percents
- value := math.Abs(number * float64(format.multiplier))
- stringValue := ""
- // get the initial string value, with the maximum # decimal digits
- if format.maxDecimalDigits >= 0 {
- stringValue = numberRound(value, format.maxDecimalDigits)
- } else {
- stringValue = fmt.Sprintf("%f", value)
- }
- // separate the integer from the decimal parts
- pos := strings.Index(stringValue, ".")
- integer := stringValue
- decimal := ""
- if pos != -1 {
- integer = stringValue[:pos]
- decimal = stringValue[pos+1:]
- }
- // make sure the minimum # decimal digits are there
- for len(decimal) < format.minDecimalDigits {
- decimal = decimal + "0"
- }
- // make sure the minimum # integer digits are there
- for len(integer) < format.minIntegerDigits {
- integer = "0" + integer
- }
- // if there's a decimal portion, prepend the decimal point symbol
- if len(decimal) > 0 {
- decimal = string(n.Symbols.Decimal) + decimal
- }
- // put the integer portion into properly sized groups
- if format.groupSizeFinal > 0 && len(integer) > format.groupSizeFinal {
- if len(integer) > format.groupSizeMain {
- groupFinal := integer[len(integer)-format.groupSizeFinal:]
- groupFirst := integer[:len(integer)-format.groupSizeFinal]
- integer = strings.Join(
- chunkString(groupFirst, format.groupSizeMain),
- n.Symbols.Group,
- ) + n.Symbols.Group + groupFinal
- }
- }
- // append/prepend negative/positive prefix/suffix
- formatted := ""
- if negative {
- formatted = format.negativePrefix + integer + decimal + format.negativeSuffix
- } else {
- formatted = format.positivePrefix + integer + decimal + format.positiveSuffix
- }
- // replace percents and permilles with the local symbols (likely to be exactly the same)
- formatted = strings.Replace(formatted, "%", string(n.Symbols.Percent), -1)
- formatted = strings.Replace(formatted, "‰", string(n.Symbols.PerMille), -1)
- return formatted
- }
- // chunkString takes a string and chunks it into size-sized pieces in a slice.
- // If the length of the string is not divisible by the size, then the first
- // chunk in the slice will be padded to compensate.
- func chunkString(str string, size int) []string {
- if str == "" {
- return []string{}
- }
- if size == 0 {
- return []string{str}
- }
- chunks := make([]string, int64(math.Ceil(float64(len(str))/float64(size))))
- for len(str) < len(chunks)*size {
- str = " " + str
- }
- for i := 0; i < len(chunks); i++ {
- start := i * size
- stop := int64(math.Min(float64(start+size), float64(len(str))))
- chunks[i] = str[start:stop]
- }
- chunks[0] = strings.TrimLeft(chunks[0], " ")
- return chunks
- }
- // numberRound takes a number and returns a string containing a rounded to the
- // even with the number of decimal places requested. If this would result in
- // the right most decimal place(s) containing "0"s, then all "0"s on the end of
- // the decimal portion will be truncated.
- func numberRound(number float64, decimals int) string {
- if number == float64(int64(number)) {
- return strconv.FormatInt(int64(number), 10)
- }
- str := fmt.Sprintf("%f", number)
- pos := strings.Index(str, ".")
- if pos != -1 && len(str) > (pos+decimals) {
- str = str[0 : pos+decimals+1]
- }
- backToNum, _ := strconv.ParseFloat(str, 64)
- difference := number - backToNum
- half := 0.5
- for i := 0; i < decimals; i++ {
- half = half / 10
- }
- roundUp := false
- if difference > half {
- roundUp = true
- } else if difference == half {
- // for halfs, round to even
- lastDigit := str[:len(str)-1]
- roundUp = lastDigit == "1" || lastDigit == "3" || lastDigit == "5" || lastDigit == "7" || lastDigit == "9"
- }
- if roundUp {
- // multiply, then ceil, then divide
- multiplier := math.Pow(float64(10), float64(decimals))
- multiplied := strconv.FormatFloat(math.Ceil(number*multiplier), 'f', 0, 64)
- if len(multiplied) > decimals {
- str = multiplied[:len(multiplied)-decimals] + "." + multiplied[len(multiplied)-decimals:]
- } else {
- str = "0." + strings.Repeat("0", decimals-len(multiplied)) + multiplied
- }
- }
- str = strings.TrimRight(str, "0")
- str = strings.TrimRight(str, ".")
- return str
- }
|