number.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. package ut
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "math"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. )
  12. // numberFormat is a struct that contains all the information about number
  13. // formatting for a specific locale that we need to do number, currency, and
  14. // percentage formatting
  15. type numberFormat struct {
  16. positivePrefix string
  17. positiveSuffix string
  18. negativePrefix string
  19. negativeSuffix string
  20. multiplier int
  21. minDecimalDigits int
  22. maxDecimalDigits int
  23. minIntegerDigits int
  24. groupSizeFinal int // only the right-most (least significant) group
  25. groupSizeMain int // all other groups
  26. }
  27. // CurrencyType is the type of Currency your converting
  28. type CurrencyType int
  29. // Currency Types such as Standard vs Accounting notation i.e. -$123.50 vs ($123.50)
  30. const (
  31. CurrencyStandard CurrencyType = iota
  32. CurrencyAccounting
  33. )
  34. var (
  35. // numberFormats keeps a copy of all numberFormat instances that have been
  36. // loaded before, to prevent parsing a single number format string multiple
  37. // times. There is vey little danger of this list consuming too much memory,
  38. // since the data for each of these is pretty small in size, and the same
  39. // formats are used by multiple locales.
  40. numberFormats = map[string]*numberFormat{}
  41. numberFormatsNoDecimals = map[string]*numberFormat{}
  42. nfMutex = new(sync.RWMutex)
  43. nfndMutex = new(sync.RWMutex)
  44. // prefixSuffixRegex is a regular expression that is used to parse number
  45. // formats
  46. prefixSuffixRegex = regexp.MustCompile(`(.*?)[#,\.0]+(.*)`)
  47. )
  48. func getCurrencyPattern(typ CurrencyType, nf NumberFormats) string {
  49. if typ == CurrencyStandard {
  50. return nf.Currency
  51. }
  52. return nf.CurrencyAccounting
  53. }
  54. // FmtCurrency takes a float number and a currency key and returns a string
  55. // with a properly formatted currency amount with the correct currency symbol.
  56. // If a symbol cannot be found for the reqested currency, this will return blank,
  57. // use FmtCurrencySafe for variant.
  58. func (n Number) FmtCurrency(typ CurrencyType, currency string, number float64) string {
  59. formatted, err := n.FmtCurrencySafe(typ, currency, number)
  60. if err != nil {
  61. fmt.Println(err)
  62. }
  63. return formatted
  64. }
  65. // FmtCurrencySafe takes a float number and a currency key and returns a string
  66. // with a properly formatted currency amount with the correct currency symbol.
  67. // If a symbol cannot be found for the reqested currency, the the key is used
  68. // instead. If the currency key requested is not recognized, it is used as the
  69. // symbol, and an error is returned with the formatted string.
  70. func (n Number) FmtCurrencySafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {
  71. format := n.parseFormat(getCurrencyPattern(typ, n.Formats), true)
  72. result := n.formatNumber(format, number)
  73. c, ok := n.Currencies[currency]
  74. if !ok {
  75. s := "**** WARNING **** unknown currency: " + currency
  76. err = errors.New(s)
  77. log.Println(s)
  78. formatted = strings.Replace(result, "¤", currency, -1)
  79. return
  80. }
  81. formatted = strings.Replace(result, "¤", c.Symbol, -1)
  82. return
  83. }
  84. // FmtCurrencyWhole does exactly what FormatCurrency does, but it leaves off
  85. // any decimal places. AKA, it would return $100 rather than $100.00.
  86. // If a symbol cannot be found for the reqested currency, this will panic, use
  87. // FmtCurrencyWholeSafe for non panicing variant.
  88. func (n Number) FmtCurrencyWhole(typ CurrencyType, currency string, number float64) string {
  89. formatted, err := n.FmtCurrencyWholeSafe(typ, currency, number)
  90. if err != nil {
  91. fmt.Println(err)
  92. }
  93. return formatted
  94. }
  95. // FmtCurrencyWholeSafe does exactly what FormatCurrency does, but it leaves off
  96. // any decimal places. AKA, it would return $100 rather than $100.00.
  97. func (n Number) FmtCurrencyWholeSafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {
  98. format := n.parseFormat(getCurrencyPattern(typ, n.Formats), false)
  99. result := n.formatNumber(format, number)
  100. c, ok := n.Currencies[currency]
  101. if !ok {
  102. s := "**** WARNING **** unknown currency: " + currency
  103. err = errors.New(s)
  104. log.Println(s)
  105. formatted = strings.Replace(result, "¤", currency, -1)
  106. return
  107. }
  108. formatted = strings.Replace(result, "¤", c.Symbol, -1)
  109. return
  110. }
  111. // FmtNumber takes a float number and returns a properly formatted string
  112. // representation of that number according to the locale's number format.
  113. func (n Number) FmtNumber(number float64) string {
  114. return n.formatNumber(n.parseFormat(n.Formats.Decimal, true), number)
  115. }
  116. // FmtNumberWhole does exactly what FormatNumber does, but it leaves off any
  117. // decimal places. AKA, it would return 100 rather than 100.01.
  118. func (n Number) FmtNumberWhole(number float64) string {
  119. return n.formatNumber(n.parseFormat(n.Formats.Decimal, false), number)
  120. }
  121. // FmtPercent takes a float number and returns a properly formatted string
  122. // representation of that number as a percentage according to the locale's
  123. // percentage format.
  124. func (n Number) FmtPercent(number float64) string {
  125. return n.formatNumber(n.parseFormat(n.Formats.Percent, true), number)
  126. }
  127. // parseFormat takes a format string and returns a numberFormat instance
  128. func (n Number) parseFormat(pattern string, includeDecimalDigits bool) *numberFormat {
  129. if includeDecimalDigits {
  130. nfMutex.RLock()
  131. if format, exists := numberFormats[pattern]; exists {
  132. nfMutex.RUnlock()
  133. return format
  134. }
  135. nfMutex.RUnlock()
  136. } else {
  137. nfndMutex.RLock()
  138. if format, exists := numberFormatsNoDecimals[pattern]; exists {
  139. nfndMutex.RUnlock()
  140. return format
  141. }
  142. nfndMutex.RUnlock()
  143. }
  144. format := new(numberFormat)
  145. patterns := strings.Split(pattern, ";")
  146. matches := prefixSuffixRegex.FindAllStringSubmatch(patterns[0], -1)
  147. if len(matches) > 0 {
  148. if len(matches[0]) > 1 {
  149. format.positivePrefix = matches[0][1]
  150. }
  151. if len(matches[0]) > 2 {
  152. format.positiveSuffix = matches[0][2]
  153. }
  154. }
  155. // default values for negative prefix & suffix
  156. format.negativePrefix = string(n.Symbols.Negative) + string(format.positivePrefix)
  157. format.negativeSuffix = format.positiveSuffix
  158. // see if they are in the pattern
  159. if len(patterns) > 1 {
  160. matches = prefixSuffixRegex.FindAllStringSubmatch(patterns[1], -1)
  161. if len(matches) > 0 {
  162. if len(matches[0]) > 1 {
  163. format.negativePrefix = matches[0][1]
  164. }
  165. if len(matches[0]) > 2 {
  166. format.negativeSuffix = matches[0][2]
  167. }
  168. }
  169. }
  170. pat := patterns[0]
  171. if strings.Index(pat, "%") != -1 {
  172. format.multiplier = 100
  173. } else if strings.Index(pat, "‰") != -1 {
  174. format.multiplier = 1000
  175. } else {
  176. format.multiplier = 1
  177. }
  178. pos := strings.Index(pat, ".")
  179. if pos != -1 {
  180. pos2 := strings.LastIndex(pat, "0")
  181. if pos2 > pos {
  182. format.minDecimalDigits = pos2 - pos
  183. }
  184. pos3 := strings.LastIndex(pat, "#")
  185. if pos3 >= pos2 {
  186. format.maxDecimalDigits = pos3 - pos
  187. } else {
  188. format.maxDecimalDigits = format.minDecimalDigits
  189. }
  190. pat = pat[0:pos]
  191. }
  192. p := strings.Replace(pat, ",", "", -1)
  193. pos = strings.Index(p, "0")
  194. if pos != -1 {
  195. format.minIntegerDigits = strings.LastIndex(p, "0") - pos + 1
  196. }
  197. p = strings.Replace(pat, "#", "0", -1)
  198. pos = strings.LastIndex(pat, ",")
  199. if pos != -1 {
  200. format.groupSizeFinal = strings.LastIndex(p, "0") - pos
  201. pos2 := strings.LastIndex(p[0:pos], ",")
  202. if pos2 != -1 {
  203. format.groupSizeMain = pos - pos2 - 1
  204. } else {
  205. format.groupSizeMain = format.groupSizeFinal
  206. }
  207. }
  208. if includeDecimalDigits {
  209. nfMutex.Lock()
  210. numberFormats[pattern] = format
  211. nfMutex.Unlock()
  212. return format
  213. }
  214. format.maxDecimalDigits = 0
  215. format.minDecimalDigits = 0
  216. nfndMutex.Lock()
  217. numberFormatsNoDecimals[pattern] = format
  218. nfndMutex.Unlock()
  219. return format
  220. }
  221. // formatNumber takes an arbitrary numberFormat and a number and applies that
  222. // format to that number, returning the resulting string
  223. func (n Number) formatNumber(format *numberFormat, number float64) string {
  224. negative := number < 0
  225. // apply the multiplier first - this is mainly used for percents
  226. value := math.Abs(number * float64(format.multiplier))
  227. stringValue := ""
  228. // get the initial string value, with the maximum # decimal digits
  229. if format.maxDecimalDigits >= 0 {
  230. stringValue = numberRound(value, format.maxDecimalDigits)
  231. } else {
  232. stringValue = fmt.Sprintf("%f", value)
  233. }
  234. // separate the integer from the decimal parts
  235. pos := strings.Index(stringValue, ".")
  236. integer := stringValue
  237. decimal := ""
  238. if pos != -1 {
  239. integer = stringValue[:pos]
  240. decimal = stringValue[pos+1:]
  241. }
  242. // make sure the minimum # decimal digits are there
  243. for len(decimal) < format.minDecimalDigits {
  244. decimal = decimal + "0"
  245. }
  246. // make sure the minimum # integer digits are there
  247. for len(integer) < format.minIntegerDigits {
  248. integer = "0" + integer
  249. }
  250. // if there's a decimal portion, prepend the decimal point symbol
  251. if len(decimal) > 0 {
  252. decimal = string(n.Symbols.Decimal) + decimal
  253. }
  254. // put the integer portion into properly sized groups
  255. if format.groupSizeFinal > 0 && len(integer) > format.groupSizeFinal {
  256. if len(integer) > format.groupSizeMain {
  257. groupFinal := integer[len(integer)-format.groupSizeFinal:]
  258. groupFirst := integer[:len(integer)-format.groupSizeFinal]
  259. integer = strings.Join(
  260. chunkString(groupFirst, format.groupSizeMain),
  261. n.Symbols.Group,
  262. ) + n.Symbols.Group + groupFinal
  263. }
  264. }
  265. // append/prepend negative/positive prefix/suffix
  266. formatted := ""
  267. if negative {
  268. formatted = format.negativePrefix + integer + decimal + format.negativeSuffix
  269. } else {
  270. formatted = format.positivePrefix + integer + decimal + format.positiveSuffix
  271. }
  272. // replace percents and permilles with the local symbols (likely to be exactly the same)
  273. formatted = strings.Replace(formatted, "%", string(n.Symbols.Percent), -1)
  274. formatted = strings.Replace(formatted, "‰", string(n.Symbols.PerMille), -1)
  275. return formatted
  276. }
  277. // chunkString takes a string and chunks it into size-sized pieces in a slice.
  278. // If the length of the string is not divisible by the size, then the first
  279. // chunk in the slice will be padded to compensate.
  280. func chunkString(str string, size int) []string {
  281. if str == "" {
  282. return []string{}
  283. }
  284. if size == 0 {
  285. return []string{str}
  286. }
  287. chunks := make([]string, int64(math.Ceil(float64(len(str))/float64(size))))
  288. for len(str) < len(chunks)*size {
  289. str = " " + str
  290. }
  291. for i := 0; i < len(chunks); i++ {
  292. start := i * size
  293. stop := int64(math.Min(float64(start+size), float64(len(str))))
  294. chunks[i] = str[start:stop]
  295. }
  296. chunks[0] = strings.TrimLeft(chunks[0], " ")
  297. return chunks
  298. }
  299. // numberRound takes a number and returns a string containing a rounded to the
  300. // even with the number of decimal places requested. If this would result in
  301. // the right most decimal place(s) containing "0"s, then all "0"s on the end of
  302. // the decimal portion will be truncated.
  303. func numberRound(number float64, decimals int) string {
  304. if number == float64(int64(number)) {
  305. return strconv.FormatInt(int64(number), 10)
  306. }
  307. str := fmt.Sprintf("%f", number)
  308. pos := strings.Index(str, ".")
  309. if pos != -1 && len(str) > (pos+decimals) {
  310. str = str[0 : pos+decimals+1]
  311. }
  312. backToNum, _ := strconv.ParseFloat(str, 64)
  313. difference := number - backToNum
  314. half := 0.5
  315. for i := 0; i < decimals; i++ {
  316. half = half / 10
  317. }
  318. roundUp := false
  319. if difference > half {
  320. roundUp = true
  321. } else if difference == half {
  322. // for halfs, round to even
  323. lastDigit := str[:len(str)-1]
  324. roundUp = lastDigit == "1" || lastDigit == "3" || lastDigit == "5" || lastDigit == "7" || lastDigit == "9"
  325. }
  326. if roundUp {
  327. // multiply, then ceil, then divide
  328. multiplier := math.Pow(float64(10), float64(decimals))
  329. multiplied := strconv.FormatFloat(math.Ceil(number*multiplier), 'f', 0, 64)
  330. if len(multiplied) > decimals {
  331. str = multiplied[:len(multiplied)-decimals] + "." + multiplied[len(multiplied)-decimals:]
  332. } else {
  333. str = "0." + strings.Repeat("0", decimals-len(multiplied)) + multiplied
  334. }
  335. }
  336. str = strings.TrimRight(str, "0")
  337. str = strings.TrimRight(str, ".")
  338. return str
  339. }