From 93b3b1386714999d716e03b131f77234248f1724 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Wed, 28 Dec 2016 22:09:31 -0600 Subject: [PATCH] tpl/lang: Add NumFmt function NumFmt formats a number with a given precision using the requested decimal, grouping, and negative characters. Fixes #1444 --- docs/content/templates/functions.md | 18 ++++++ tpl/lang/init.go | 10 +++ tpl/lang/lang.go | 95 +++++++++++++++++++++++++++++ tpl/lang/lang_test.go | 54 ++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 tpl/lang/lang_test.go diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index c1fd8ebad..c514a4ec8 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -460,6 +460,24 @@ e.g. * `{{ int "123" }}` → 123 +### lang.NumFmt + +`NumFmt` formats a number with the given precision using the *decimal*, +*grouping*, and *negative* options. The `options` parameter is a +string consisting of ` `. The default +`options` value is `- . ,`. + +Note that numbers are rounded up at 5 or greater. +So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`. + +``` +{{ lang.NumFmt 2 12345.6789 }} → 12,345.68 +{{ lang.NumFmt 2 12345.6789 "- , ." }} → 12.345,68 +{{ lang.NumFmt 0 -12345.6789 "- . ," }} → -12,346 +{{ lang.NumFmt 6 -12345.6789 "- ." }} → -12345.678900 +{{ -98765.4321 | lang.NumFmt 2 }} → -98,765.43 +``` + ## Strings ### printf diff --git a/tpl/lang/init.go b/tpl/lang/init.go index 6cf8e790d..ea67afab0 100644 --- a/tpl/lang/init.go +++ b/tpl/lang/init.go @@ -34,6 +34,16 @@ func init() { [][2]string{}, ) + ns.AddMethodMapping(ctx.NumFmt, + nil, + [][2]string{ + {`{{ lang.NumFmt 2 12345.6789 }}`, `12,345.68`}, + {`{{ lang.NumFmt 2 12345.6789 "- , ." }}`, `12.345,68`}, + {`{{ lang.NumFmt 6 -12345.6789 "- ." }}`, `-12345.678900`}, + {`{{ lang.NumFmt 0 -12345.6789 "- . ," }}`, `-12,346`}, + {`{{ -98765.4321 | lang.NumFmt 2 }}`, `-98,765.43`}, + }, + ) return ns } diff --git a/tpl/lang/lang.go b/tpl/lang/lang.go index c84728f3b..45a640d7a 100644 --- a/tpl/lang/lang.go +++ b/tpl/lang/lang.go @@ -14,6 +14,11 @@ package lang import ( + "errors" + "math" + "strconv" + "strings" + "github.com/spf13/cast" "github.com/spf13/hugo/deps" ) @@ -39,3 +44,93 @@ func (ns *Namespace) Translate(id interface{}, args ...interface{}) (string, err return ns.deps.Translate(sid, args...), nil } + +// NumFmt formats a number with the given precision using the +// negative, decimal, and grouping options. The `options` +// parameter is a string consisting of ` `. The +// default `options` value is `- . ,`. +// +// Note that numbers are rounded up at 5 or greater. +// So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`. +func (ns *Namespace) NumFmt(precision, number interface{}, options ...interface{}) (string, error) { + prec, err := cast.ToIntE(precision) + if err != nil { + return "", err + } + + n, err := cast.ToFloat64E(number) + if err != nil { + return "", err + } + + var neg, dec, grp string + + if len(options) == 0 { + // TODO(moorereason): move to site config + neg, dec, grp = "-", ".", "," + } else { + s, err := cast.ToStringE(options[0]) + if err != nil { + return "", nil + } + + rs := strings.Fields(s) + switch len(rs) { + case 0: + case 1: + neg = rs[0] + case 2: + neg, dec = rs[0], rs[1] + case 3: + neg, dec, grp = rs[0], rs[1], rs[2] + default: + return "", errors.New("too many fields in options parameter to NumFmt") + } + } + + // Logic from MIT Licensed github.com/go-playground/locales/ + // Original Copyright (c) 2016 Go Playground + + s := strconv.FormatFloat(math.Abs(n), 'f', prec, 64) + L := len(s) + 2 + len(s[:len(s)-1-prec])/3 + + var count int + inWhole := prec == 0 + b := make([]byte, 0, L) + + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '.' { + for j := len(dec) - 1; j >= 0; j-- { + b = append(b, dec[j]) + } + inWhole = true + continue + } + + if inWhole { + if count == 3 { + for j := len(grp) - 1; j >= 0; j-- { + b = append(b, grp[j]) + } + count = 1 + } else { + count++ + } + } + + b = append(b, s[i]) + } + + if n < 0 { + for j := len(neg) - 1; j >= 0; j-- { + b = append(b, neg[j]) + } + } + + // reverse + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + + return string(b), nil +} diff --git a/tpl/lang/lang_test.go b/tpl/lang/lang_test.go new file mode 100644 index 000000000..45ed506b7 --- /dev/null +++ b/tpl/lang/lang_test.go @@ -0,0 +1,54 @@ +package lang + +import ( + "fmt" + "testing" + + "github.com/spf13/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNumFormat(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + cases := []struct { + prec int + n float64 + runes string + + want string + }{ + {2, -12345.6789, "", "-12,345.68"}, + {2, -12345.6789, "- . ,", "-12,345.68"}, + {2, -12345.1234, "- . ,", "-12,345.12"}, + + {2, 12345.6789, "- . ,", "12,345.68"}, + {0, 12345.6789, "- . ,", "12,346"}, + {11, -12345.6789, "- . ,", "-12,345.67890000000"}, + + {3, -12345.6789, "- ,", "-12345,679"}, + {6, -12345.6789, "- , .", "-12.345,678900"}, + + // Arabic, ar_AE + {6, -12345.6789, "‏- ٫ ٬", "‏-12٬345٫678900"}, + } + + for i, c := range cases { + errMsg := fmt.Sprintf("[%d] %v", i, c) + + var s string + var err error + + if len(c.runes) == 0 { + s, err = ns.NumFmt(c.prec, c.n) + } else { + s, err = ns.NumFmt(c.prec, c.n, c.runes) + } + + require.NoError(t, err, errMsg) + assert.Equal(t, c.want, s, errMsg) + } +}