Skip to content

Commit 93b3b13

Browse files
moorereasonbep
authored andcommitted
tpl/lang: Add NumFmt function
NumFmt formats a number with a given precision using the requested decimal, grouping, and negative characters. Fixes #1444
1 parent e92ce83 commit 93b3b13

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

‎docs/content/templates/functions.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,24 @@ e.g.
460460

461461
* `{{ int "123" }}` → 123
462462

463+
### lang.NumFmt
464+
465+
`NumFmt` formats a number with the given precision using the *decimal*,
466+
*grouping*, and *negative* options. The `options` parameter is a
467+
string consisting of `<negative> <decimal> <grouping>`. The default
468+
`options` value is `- . ,`.
469+
470+
Note that numbers are rounded up at 5 or greater.
471+
So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.
472+
473+
```
474+
{{ lang.NumFmt 2 12345.6789 }} → 12,345.68
475+
{{ lang.NumFmt 2 12345.6789 "- , ." }} → 12.345,68
476+
{{ lang.NumFmt 0 -12345.6789 "- . ," }} → -12,346
477+
{{ lang.NumFmt 6 -12345.6789 "- ." }} → -12345.678900
478+
{{ -98765.4321 | lang.NumFmt 2 }} → -98,765.43
479+
```
480+
463481
## Strings
464482

465483
### printf

‎tpl/lang/init.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ func init() {
3434
[][2]string{},
3535
)
3636

37+
ns.AddMethodMapping(ctx.NumFmt,
38+
nil,
39+
[][2]string{
40+
{`{{ lang.NumFmt 2 12345.6789 }}`, `12,345.68`},
41+
{`{{ lang.NumFmt 2 12345.6789 "- , ." }}`, `12.345,68`},
42+
{`{{ lang.NumFmt 6 -12345.6789 "- ." }}`, `-12345.678900`},
43+
{`{{ lang.NumFmt 0 -12345.6789 "- . ," }}`, `-12,346`},
44+
{`{{ -98765.4321 | lang.NumFmt 2 }}`, `-98,765.43`},
45+
},
46+
)
3747
return ns
3848

3949
}

‎tpl/lang/lang.go‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
package lang
1515

1616
import (
17+
"errors"
18+
"math"
19+
"strconv"
20+
"strings"
21+
1722
"github.com/spf13/cast"
1823
"github.com/spf13/hugo/deps"
1924
)
@@ -39,3 +44,93 @@ func (ns *Namespace) Translate(id interface{}, args ...interface{}) (string, err
3944

4045
return ns.deps.Translate(sid, args...), nil
4146
}
47+
48+
// NumFmt formats a number with the given precision using the
49+
// negative, decimal, and grouping options. The `options`
50+
// parameter is a string consisting of `<negative> <decimal> <grouping>`. The
51+
// default `options` value is `- . ,`.
52+
//
53+
// Note that numbers are rounded up at 5 or greater.
54+
// So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.
55+
func (ns *Namespace) NumFmt(precision, number interface{}, options ...interface{}) (string, error) {
56+
prec, err := cast.ToIntE(precision)
57+
if err != nil {
58+
return "", err
59+
}
60+
61+
n, err := cast.ToFloat64E(number)
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
var neg, dec, grp string
67+
68+
if len(options) == 0 {
69+
// TODO(moorereason): move to site config
70+
neg, dec, grp = "-", ".", ","
71+
} else {
72+
s, err := cast.ToStringE(options[0])
73+
if err != nil {
74+
return "", nil
75+
}
76+
77+
rs := strings.Fields(s)
78+
switch len(rs) {
79+
case 0:
80+
case 1:
81+
neg = rs[0]
82+
case 2:
83+
neg, dec = rs[0], rs[1]
84+
case 3:
85+
neg, dec, grp = rs[0], rs[1], rs[2]
86+
default:
87+
return "", errors.New("too many fields in options parameter to NumFmt")
88+
}
89+
}
90+
91+
// Logic from MIT Licensed github.com/go-playground/locales/
92+
// Original Copyright (c) 2016 Go Playground
93+
94+
s := strconv.FormatFloat(math.Abs(n), 'f', prec, 64)
95+
L := len(s) + 2 + len(s[:len(s)-1-prec])/3
96+
97+
var count int
98+
inWhole := prec == 0
99+
b := make([]byte, 0, L)
100+
101+
for i := len(s) - 1; i >= 0; i-- {
102+
if s[i] == '.' {
103+
for j := len(dec) - 1; j >= 0; j-- {
104+
b = append(b, dec[j])
105+
}
106+
inWhole = true
107+
continue
108+
}
109+
110+
if inWhole {
111+
if count == 3 {
112+
for j := len(grp) - 1; j >= 0; j-- {
113+
b = append(b, grp[j])
114+
}
115+
count = 1
116+
} else {
117+
count++
118+
}
119+
}
120+
121+
b = append(b, s[i])
122+
}
123+
124+
if n < 0 {
125+
for j := len(neg) - 1; j >= 0; j-- {
126+
b = append(b, neg[j])
127+
}
128+
}
129+
130+
// reverse
131+
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
132+
b[i], b[j] = b[j], b[i]
133+
}
134+
135+
return string(b), nil
136+
}

‎tpl/lang/lang_test.go‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package lang
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/spf13/hugo/deps"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNumFormat(t *testing.T) {
13+
t.Parallel()
14+
15+
ns := New(&deps.Deps{})
16+
17+
cases := []struct {
18+
prec int
19+
n float64
20+
runes string
21+
22+
want string
23+
}{
24+
{2, -12345.6789, "", "-12,345.68"},
25+
{2, -12345.6789, "- . ,", "-12,345.68"},
26+
{2, -12345.1234, "- . ,", "-12,345.12"},
27+
28+
{2, 12345.6789, "- . ,", "12,345.68"},
29+
{0, 12345.6789, "- . ,", "12,346"},
30+
{11, -12345.6789, "- . ,", "-12,345.67890000000"},
31+
32+
{3, -12345.6789, "- ,", "-12345,679"},
33+
{6, -12345.6789, "- , .", "-12.345,678900"},
34+
35+
// Arabic, ar_AE
36+
{6, -12345.6789, "‏- ٫ ٬", "‏-12٬345٫678900"},
37+
}
38+
39+
for i, c := range cases {
40+
errMsg := fmt.Sprintf("[%d] %v", i, c)
41+
42+
var s string
43+
var err error
44+
45+
if len(c.runes) == 0 {
46+
s, err = ns.NumFmt(c.prec, c.n)
47+
} else {
48+
s, err = ns.NumFmt(c.prec, c.n, c.runes)
49+
}
50+
51+
require.NoError(t, err, errMsg)
52+
assert.Equal(t, c.want, s, errMsg)
53+
}
54+
}

0 commit comments

Comments
 (0)