number.go 11 KB

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