├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── numfmt.go └── numfmt_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | 17 | - name: Set up Go 1.x 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: ^1.15 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Test 26 | run: go test -race -v ./... 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Jack Christensen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/jackc/numfmt.svg)](https://pkg.go.dev/github.com/jackc/numfmt) 2 | ![CI](https://github.com/jackc/numfmt/workflows/CI/badge.svg) 3 | 4 | # numfmt 5 | 6 | `numfmt` is a number formatting package for Go. 7 | 8 | ## Features 9 | 10 | * Rounding to N decimal places 11 | * Always display minimum of N decimal places 12 | * Configurable thousands separators 13 | * Scaling for percentage formatting 14 | * Format negative values differently for correct currency output like `-$12.34` or `(12.34)` 15 | * Easy to use with `text/template` and `html/template` 16 | 17 | ## Examples 18 | 19 | Use directly from Go: 20 | 21 | ```go 22 | f := &numfmt.Formatter{ 23 | NegativeTemplate: "(n)", 24 | MinDecimalPlaces: 2, 25 | } 26 | f.Format("-1234") // => "(1,234.00)" 27 | ``` 28 | 29 | Or in use in `text/template`: 30 | 31 | ``` 32 | {{numfmt "1234.5"}} => "1,234.5" 33 | {{numfmt "GroupSeparator" " " "DecimalSeparator" "," "1234.5"}} => "1 234,5" 34 | ``` 35 | 36 | See the [documentation](https://pkg.go.dev/github.com/jackc/numfmt) for more examples. 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jackc/numfmt 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/shopspring/decimal v1.2.0 7 | github.com/stretchr/testify v1.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 6 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /numfmt.go: -------------------------------------------------------------------------------- 1 | // Package numfmt is a number formatting system. 2 | /* 3 | Number formatting is provided by the Formatter type. The zero value of Formatter will format numbers with commas every 4 | 3 digits and a period between the integer and fractional parts of a number. 5 | 6 | f := &numfmt.Formatter{} 7 | f.Format("1234.56789") => 1,234.56789 8 | */ 9 | package numfmt 10 | 11 | import ( 12 | "fmt" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | 17 | "github.com/shopspring/decimal" 18 | ) 19 | 20 | type Rounder struct { 21 | Places int32 // Number of decimal places to round to. 22 | } 23 | 24 | func (r *Rounder) Round(d decimal.Decimal) decimal.Decimal { 25 | return d.Round(r.Places) 26 | } 27 | 28 | // Formatter is a formatter of numbers. The zero value is usable. Do not change or copy a Formatter after it has been 29 | // used. The methods on Format are concurrency safe. 30 | type Formatter struct { 31 | GroupSeparator string // Separator to place between groups of digits. Default: "," 32 | GroupSize int // Number of digits in a group. Default: 3 33 | DecimalSeparator string // Default: "." 34 | Rounder *Rounder 35 | 36 | // Number of places to shift decimal places to the left. Negative numbers are shifted to the right. If set to 2 this 37 | // will convert a fraction to a percentage. 38 | Shift int32 39 | 40 | MinDecimalPlaces int32 // Minimum number of decimal places to display. 41 | 42 | // Template is a simple format string. All text other than format verbs is passed through unmodified. Backslash '\' 43 | // escaping can be used to include a character otherwise used as a verb. You must include '-' or '+' to have show 44 | // the sign. 45 | // 46 | // Verbs: 47 | // n the number 48 | // - optional negative sign 49 | // + always include sign 50 | // 51 | // Examples: 52 | // "n" => 9.45 53 | // "- n" => - 9.45 54 | // "+n" => +9.45 55 | // "n +" => 9.45 + 56 | // "-$n" => -$9.45 57 | // "n%" => 9.45% 58 | // 59 | // Default: "n" 60 | Template string 61 | compiledTemplate compiledTemplate 62 | 63 | // NegativeTemplate will be used if present instead of Template for negative values. The primary expected use is for 64 | // negative values surrounded by parentheses. It uses the same verbs as Template. 65 | // 66 | // Examples: 67 | // "(n)" => (9.45) 68 | // Default: "" 69 | NegativeTemplate string 70 | compiledNegativeTemplate compiledTemplate 71 | 72 | compileTemplateOnce sync.Once 73 | } 74 | 75 | // Format formats v. v can be anything that fmt.Sprint can convert to a parsable number. 76 | func (f *Formatter) Format(v interface{}) string { 77 | switch v := v.(type) { 78 | case decimal.Decimal: 79 | return f.formatDecimal(v) 80 | case string: 81 | d, err := decimal.NewFromString(v) 82 | if err != nil { 83 | return fmt.Sprint(v) 84 | } 85 | return f.formatDecimal(d) 86 | case int32: 87 | return f.formatDecimal(decimal.NewFromInt32(v)) 88 | case int64: 89 | return f.formatDecimal(decimal.NewFromInt(v)) 90 | default: 91 | s := fmt.Sprint(v) 92 | d, err := decimal.NewFromString(s) 93 | if err != nil { 94 | return s 95 | } 96 | return f.formatDecimal(d) 97 | } 98 | } 99 | 100 | func (f *Formatter) formatDecimal(d decimal.Decimal) string { 101 | f.compileTemplateOnce.Do(f.compileTemplates) 102 | 103 | if f.Shift != 0 { 104 | d = d.Shift(f.Shift) 105 | } 106 | if f.Rounder != nil { 107 | d = d.Round(f.Rounder.Places) 108 | } 109 | 110 | parts := strings.SplitN(d.String(), ".", 2) 111 | intPart := parts[0] 112 | var fracPart string 113 | if len(parts) == 2 { 114 | fracPart = parts[1] 115 | } 116 | 117 | neg := false 118 | if intPart[0] == '-' { 119 | neg = true 120 | intPart = intPart[1:] 121 | } 122 | 123 | if len(fracPart) < int(f.MinDecimalPlaces) { 124 | buf := make([]byte, int(f.MinDecimalPlaces)) 125 | copy(buf, fracPart) 126 | for i := len(fracPart); i < len(buf); i++ { 127 | buf[i] = '0' 128 | } 129 | fracPart = string(buf) 130 | } 131 | 132 | sb := &strings.Builder{} 133 | if neg && f.compiledNegativeTemplate != nil { 134 | f.compiledNegativeTemplate.write(sb, f, neg, intPart, fracPart) 135 | } else { 136 | f.compiledTemplate.write(sb, f, neg, intPart, fracPart) 137 | } 138 | 139 | return sb.String() 140 | } 141 | 142 | func (f *Formatter) compileTemplates() { 143 | if f.compiledTemplate != nil { 144 | return 145 | } 146 | 147 | t := "-n" 148 | if f.Template != "" { 149 | t = f.Template 150 | } 151 | f.compiledTemplate = compileTemplate(t) 152 | 153 | if f.NegativeTemplate == "" { 154 | return 155 | } 156 | 157 | f.compiledNegativeTemplate = compileTemplate(f.NegativeTemplate) 158 | } 159 | 160 | func writeSeparateGroups(sb *strings.Builder, num, groupSeparator string, groupSize int) { 161 | if len(groupSeparator) == 0 || groupSize == 0 || len(num) <= groupSize { 162 | sb.WriteString(num) 163 | return 164 | } 165 | 166 | sepCount := len(num) / groupSize 167 | numIdx := len(num) % groupSize 168 | if numIdx == 0 { 169 | numIdx = groupSize 170 | sepCount-- 171 | } 172 | sb.WriteString(num[:numIdx]) 173 | 174 | for i := 0; i < sepCount; i++ { 175 | sb.WriteString(groupSeparator) 176 | lastNumIdx := numIdx 177 | numIdx += groupSize 178 | sb.WriteString(num[lastNumIdx:numIdx]) 179 | } 180 | } 181 | 182 | type compiledTemplatePart interface { 183 | write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) 184 | } 185 | 186 | type compiledTemplate []compiledTemplatePart 187 | 188 | func (ct compiledTemplate) write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) { 189 | for _, part := range ct { 190 | part.write(sb, f, neg, intPart, fracPart) 191 | } 192 | } 193 | 194 | type compiledTemplatePartLiteral string 195 | 196 | func (p compiledTemplatePartLiteral) write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) { 197 | sb.WriteString(string(p)) 198 | } 199 | 200 | type compiledTemplatePartNumber struct{} 201 | 202 | func (compiledTemplatePartNumber) write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) { 203 | groupSeparator := "," 204 | if f.GroupSeparator != "" { 205 | groupSeparator = f.GroupSeparator 206 | } 207 | groupSize := 3 208 | if f.GroupSize != 0 { 209 | groupSize = f.GroupSize 210 | } 211 | writeSeparateGroups(sb, intPart, groupSeparator, groupSize) 212 | 213 | decimalSeparator := "." 214 | if f.DecimalSeparator != "" { 215 | decimalSeparator = f.DecimalSeparator 216 | } 217 | if len(fracPart) != 0 { 218 | sb.WriteString(decimalSeparator) 219 | sb.WriteString(fracPart) 220 | } 221 | } 222 | 223 | type compiledTemplatePartOptionalSign struct{} 224 | 225 | func (compiledTemplatePartOptionalSign) write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) { 226 | if neg { 227 | sb.WriteByte('-') 228 | } 229 | } 230 | 231 | type compiledTemplatePartForceSign struct{} 232 | 233 | func (compiledTemplatePartForceSign) write(sb *strings.Builder, f *Formatter, neg bool, intPart, fracPart string) { 234 | var sign byte 235 | if neg { 236 | sign = '-' 237 | } else { 238 | sign = '+' 239 | } 240 | sb.WriteByte(sign) 241 | } 242 | 243 | func compileTemplate(s string) compiledTemplate { 244 | sr := strings.NewReader(s) 245 | 246 | ct := compiledTemplate{} 247 | 248 | literal := &strings.Builder{} 249 | escape := false 250 | for { 251 | b, err := sr.ReadByte() 252 | if err != nil { 253 | if literal.Len() > 0 { 254 | ct = append(ct, compiledTemplatePartLiteral(literal.String())) 255 | } 256 | break 257 | } 258 | 259 | if escape { 260 | literal.WriteByte(b) 261 | escape = false 262 | continue 263 | } 264 | 265 | if b == '\\' { 266 | escape = true 267 | continue 268 | } 269 | 270 | if b == 'n' || b == '-' || b == '+' { 271 | if literal.Len() > 0 { 272 | ct = append(ct, compiledTemplatePartLiteral(literal.String())) 273 | literal.Reset() 274 | } 275 | 276 | switch b { 277 | case 'n': 278 | ct = append(ct, compiledTemplatePartNumber{}) 279 | case '-': 280 | ct = append(ct, compiledTemplatePartOptionalSign{}) 281 | case '+': 282 | ct = append(ct, compiledTemplatePartForceSign{}) 283 | } 284 | } else { 285 | literal.WriteByte(b) 286 | } 287 | } 288 | 289 | return ct 290 | } 291 | 292 | // TemplateFunc is a helper method for use with text/template and html/template. args is a sequence of key-value pairs 293 | // configuring the formatting. If len(args) is even a formatting function is returned. If len(args) is odd the final 294 | // value is formatted and returned. 295 | // 296 | // Keys are generally named the same as matching the Formatter fields: 297 | // GroupSeparator 298 | // GroupSize 299 | // DecimalSeparator 300 | // RoundPlaces 301 | // Shift 302 | // MinDecimalPlaces 303 | // Template 304 | // NegativeTemplate 305 | func TemplateFunc(args ...interface{}) (interface{}, error) { 306 | f := &Formatter{} 307 | for i := 0; i < len(args)-1; i += 2 { 308 | key := args[i] 309 | strValue := fmt.Sprint(args[i+1]) 310 | 311 | switch key { 312 | case "GroupSeparator": 313 | f.GroupSeparator = strValue 314 | case "GroupSize": 315 | n, err := strconv.ParseInt(strValue, 10, 64) 316 | if err != nil { 317 | return nil, err 318 | } 319 | f.GroupSize = int(n) 320 | case "DecimalSeparator": 321 | f.DecimalSeparator = strValue 322 | case "RoundPlaces": 323 | n, err := strconv.ParseInt(strValue, 10, 32) 324 | if err != nil { 325 | return nil, err 326 | } 327 | if f.Rounder == nil { 328 | f.Rounder = &Rounder{} 329 | } 330 | f.Rounder.Places = int32(n) 331 | case "Shift": 332 | n, err := strconv.ParseInt(strValue, 10, 64) 333 | if err != nil { 334 | return nil, err 335 | } 336 | f.Shift = int32(n) 337 | case "MinDecimalPlaces": 338 | n, err := strconv.ParseInt(strValue, 10, 64) 339 | if err != nil { 340 | return nil, err 341 | } 342 | f.MinDecimalPlaces = int32(n) 343 | case "Template": 344 | f.Template = strValue 345 | case "NegativeTemplate": 346 | f.NegativeTemplate = strValue 347 | default: 348 | return nil, fmt.Errorf("unknown key: %s", key) 349 | } 350 | } 351 | 352 | if len(args)%2 == 1 { 353 | return f.Format(args[len(args)-1]), nil 354 | } 355 | 356 | return f.Format, nil 357 | } 358 | 359 | // NewUSDFormatter returns a Formatter for US dollars. 360 | func NewUSDFormatter() *Formatter { 361 | return &Formatter{ 362 | MinDecimalPlaces: 2, 363 | Template: `-$n`, 364 | } 365 | } 366 | 367 | // NewPercentFormatter returns a formatter that formats a number such as 0.75 to 75%. 368 | func NewPercentFormatter() *Formatter { 369 | return &Formatter{ 370 | Shift: 2, 371 | Template: `-n%`, 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /numfmt_test.go: -------------------------------------------------------------------------------- 1 | package numfmt_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | "text/template" 9 | 10 | "github.com/jackc/numfmt" 11 | "github.com/shopspring/decimal" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | type testFormatter numfmt.Formatter 16 | 17 | func (f *testFormatter) String() string { 18 | parts := []string{} 19 | if f.GroupSeparator != "" { 20 | parts = append(parts, fmt.Sprintf(`GroupSeparator: "%s"`, f.GroupSeparator)) 21 | } 22 | if f.GroupSize != 0 { 23 | parts = append(parts, fmt.Sprintf("GroupSize: %d", f.GroupSize)) 24 | } 25 | if f.DecimalSeparator != "" { 26 | parts = append(parts, fmt.Sprintf(`DecimalSeparator: "%s"`, f.DecimalSeparator)) 27 | } 28 | if f.Rounder != nil { 29 | parts = append(parts, fmt.Sprintf(`Rounder: {Places: %d}`, f.Rounder.Places)) 30 | } 31 | if f.Template != "" { 32 | parts = append(parts, fmt.Sprintf(`Template: "%s"`, f.Template)) 33 | } 34 | if f.NegativeTemplate != "" { 35 | parts = append(parts, fmt.Sprintf(`NegativeTemplate: "%s"`, f.NegativeTemplate)) 36 | } 37 | 38 | return "&Formatter{" + strings.Join(parts, ", ") + "}" 39 | } 40 | 41 | func TestFormatterFormat(t *testing.T) { 42 | for i, tt := range []struct { 43 | formatter *numfmt.Formatter 44 | arg interface{} 45 | expected string 46 | }{ 47 | // Defaults 48 | {&numfmt.Formatter{}, "0", "0"}, 49 | {&numfmt.Formatter{}, "1", "1"}, 50 | {&numfmt.Formatter{}, "12", "12"}, 51 | {&numfmt.Formatter{}, "123", "123"}, 52 | {&numfmt.Formatter{}, "1234", "1,234"}, 53 | {&numfmt.Formatter{}, "12345", "12,345"}, 54 | {&numfmt.Formatter{}, "123456", "123,456"}, 55 | {&numfmt.Formatter{}, "12345678901234567890", "12,345,678,901,234,567,890"}, 56 | {&numfmt.Formatter{}, "1.0", "1"}, 57 | {&numfmt.Formatter{}, "1.2", "1.2"}, 58 | {&numfmt.Formatter{}, "12345.6789", "12,345.6789"}, 59 | {&numfmt.Formatter{}, "-12345.6789", "-12,345.6789"}, 60 | 61 | {&numfmt.Formatter{DecimalSeparator: ","}, "1.2", "1,2"}, 62 | {&numfmt.Formatter{GroupSeparator: " "}, "1234", "1 234"}, 63 | {&numfmt.Formatter{GroupSize: 1}, "1234", "1,2,3,4"}, 64 | 65 | {&numfmt.Formatter{Rounder: &numfmt.Rounder{Places: 0}}, "1234.1", "1,234"}, 66 | {&numfmt.Formatter{Rounder: &numfmt.Rounder{Places: 0}}, "1234.5", "1,235"}, 67 | {&numfmt.Formatter{Rounder: &numfmt.Rounder{Places: 0}}, "1234.9", "1,235"}, 68 | {&numfmt.Formatter{Rounder: &numfmt.Rounder{Places: 3}}, "1234.5678", "1,234.568"}, 69 | {&numfmt.Formatter{Rounder: &numfmt.Rounder{Places: -2}}, "1234.5678", "1,200"}, 70 | 71 | {&numfmt.Formatter{Shift: 2}, "0.31", "31"}, 72 | {&numfmt.Formatter{Shift: -1}, "42", "4.2"}, 73 | 74 | // Shift happens before rounding 75 | {&numfmt.Formatter{Shift: 2, Rounder: &numfmt.Rounder{Places: 0}}, "0.315", "32"}, 76 | 77 | {&numfmt.Formatter{MinDecimalPlaces: 2}, "123", "123.00"}, 78 | 79 | // Template 80 | {&numfmt.Formatter{Template: "+n"}, "123", "+123"}, 81 | {&numfmt.Formatter{Template: "-n"}, "123", "123"}, 82 | {&numfmt.Formatter{Template: "-n"}, "-123", "-123"}, 83 | {&numfmt.Formatter{Template: "n -"}, "-123", "123 -"}, 84 | {&numfmt.Formatter{Template: `\n \- \+ \\ n`}, "123", `n - + \ 123`}, 85 | 86 | // Negative Template 87 | {&numfmt.Formatter{NegativeTemplate: "(n)"}, "123", "123"}, 88 | {&numfmt.Formatter{NegativeTemplate: "(n)"}, "-123", "(123)"}, 89 | 90 | // Different argument type tests 91 | {&numfmt.Formatter{}, 1234, "1,234"}, 92 | {&numfmt.Formatter{}, 1234.0, "1,234"}, 93 | {&numfmt.Formatter{}, int32(1234), "1,234"}, 94 | {&numfmt.Formatter{}, int64(1234), "1,234"}, 95 | {&numfmt.Formatter{}, float32(1234.5), "1,234.5"}, 96 | {&numfmt.Formatter{}, float64(1234.5), "1,234.5"}, 97 | {&numfmt.Formatter{}, decimal.RequireFromString("1234"), "1,234"}, 98 | 99 | // Not a number 100 | {&numfmt.Formatter{}, "foobar", "foobar"}, 101 | } { 102 | actual := tt.formatter.Format(tt.arg) 103 | if tt.expected != actual { 104 | t.Errorf("%d. expected formatting %v with %v to return %v, but got %v", i, tt.arg, (*testFormatter)(tt.formatter), tt.expected, actual) 105 | } 106 | } 107 | } 108 | 109 | func TestTemplateFunc(t *testing.T) { 110 | for i, tt := range []struct { 111 | format []interface{} 112 | arg interface{} 113 | expected string 114 | }{ 115 | {[]interface{}{}, "1234.5", "1,234.5"}, 116 | {[]interface{}{"DecimalSeparator", ","}, "1.2", "1,2"}, 117 | {[]interface{}{"GroupSeparator", " "}, "1234", "1 234"}, 118 | {[]interface{}{"GroupSize", 1}, "1234", "1,2,3,4"}, 119 | {[]interface{}{"RoundPlaces", 0}, "1234.9", "1,235"}, 120 | {[]interface{}{"Shift", 2}, "0.31", "31"}, 121 | {[]interface{}{"Shift", 2, "RoundPlaces", 0}, "0.315", "32"}, 122 | {[]interface{}{"MinDecimalPlaces", 2}, "123", "123.00"}, 123 | {[]interface{}{"Template", "+n"}, "123", "+123"}, 124 | {[]interface{}{"NegativeTemplate", "(n)"}, "-123", "(123)"}, 125 | } { 126 | fn, err := numfmt.TemplateFunc(tt.format...) 127 | assert.NoError(t, err) 128 | if fn, ok := fn.(func(interface{}) string); ok { 129 | actual := fn(tt.arg) 130 | if tt.expected != actual { 131 | t.Errorf("%d. func: expected formatting %v with %v to return %v, but got %v", i, tt.arg, tt.format, tt.expected, actual) 132 | } 133 | } else { 134 | t.Errorf("%d. func: expected formatting with %v to return function but did not", i, tt.format) 135 | } 136 | 137 | args := append(tt.format, tt.arg) 138 | actual, err := numfmt.TemplateFunc(args...) 139 | assert.NoError(t, err) 140 | if tt.expected != actual { 141 | t.Errorf("%d. immediate: expected formatting %v with %v to return %v, but got %v", i, tt.arg, tt.format, tt.expected, actual) 142 | } 143 | } 144 | } 145 | 146 | func TestNewUSDFormatter(t *testing.T) { 147 | for i, tt := range []struct { 148 | arg interface{} 149 | expected string 150 | }{ 151 | {"123", "$123.00"}, 152 | {"-123", "-$123.00"}, 153 | {"123.456", "$123.456"}, 154 | } { 155 | actual := numfmt.NewUSDFormatter().Format(tt.arg) 156 | if tt.expected != actual { 157 | t.Errorf("%d. expected formatting %v to return %v, but got %v", i, tt.arg, tt.expected, actual) 158 | } 159 | } 160 | } 161 | 162 | func TestNewPercentFormatter(t *testing.T) { 163 | for i, tt := range []struct { 164 | arg interface{} 165 | expected string 166 | }{ 167 | {"0.123", "12.3%"}, 168 | {"1.5", "150%"}, 169 | {"-3", "-300%"}, 170 | } { 171 | actual := numfmt.NewPercentFormatter().Format(tt.arg) 172 | if tt.expected != actual { 173 | t.Errorf("%d. expected formatting %v to return %v, but got %v", i, tt.arg, tt.expected, actual) 174 | } 175 | } 176 | } 177 | 178 | func ExampleTemplateFunc() { 179 | t := template.New("root").Funcs(template.FuncMap{ 180 | "numfmt": numfmt.TemplateFunc, 181 | }) 182 | t = template.Must(t.Parse(` 183 | numfmt can be called directly: 184 | {{numfmt "GroupSeparator" " " "DecimalSeparator" "," "1234.56789"}} 185 | or it can return a function for later use: 186 | {{- $formatUSD := numfmt "Template" "$n" "RoundPlaces" 2 "MinDecimalPlaces" 2}} 187 | {{call $formatUSD "1234.56789"}} 188 | `)) 189 | 190 | err := t.Execute(os.Stdout, nil) 191 | if err != nil { 192 | fmt.Println(err) 193 | } 194 | 195 | // Output: 196 | // numfmt can be called directly: 197 | // 1 234,56789 198 | // or it can return a function for later use: 199 | // $1,234.57 200 | } 201 | 202 | func ExampleFormatter_zero() { 203 | f := &numfmt.Formatter{} 204 | fmt.Println(f.Format("1234.56789")) 205 | 206 | // Output: 207 | // 1,234.56789 208 | } 209 | 210 | func ExampleFormatter_rounding() { 211 | f := &numfmt.Formatter{ 212 | Rounder: &numfmt.Rounder{Places: 2}, 213 | } 214 | fmt.Println(f.Format("1234.56789")) 215 | 216 | // Output: 217 | // 1,234.57 218 | } 219 | 220 | func ExampleFormatter_negative_currency() { 221 | f := &numfmt.Formatter{ 222 | NegativeTemplate: "(n)", 223 | MinDecimalPlaces: 2, 224 | } 225 | fmt.Println(f.Format("-1234")) 226 | 227 | // Output: 228 | // (1,234.00) 229 | } 230 | 231 | func ExampleNewPercentFormatter() { 232 | f := numfmt.NewPercentFormatter() 233 | fmt.Println(f.Format("0.781")) 234 | 235 | // Output: 236 | // 78.1% 237 | } 238 | --------------------------------------------------------------------------------