shithub: hugo

Download patch

ref: 93b3b1386714999d716e03b131f77234248f1724
parent: e92ce83d5e8837190511f5a73323e49eeb8466cd
author: Cameron Moore <[email protected]>
date: Wed Dec 28 17:09:31 EST 2016

tpl/lang: Add NumFmt function

NumFmt formats a number with a given precision using the requested
decimal, grouping, and negative characters.

Fixes #1444

--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -460,6 +460,24 @@
 
 * `{{ 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 `<negative> <decimal> <grouping>`.  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
--- a/tpl/lang/init.go
+++ b/tpl/lang/init.go
@@ -34,6 +34,16 @@
 			[][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
 
 	}
--- 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"
 )
@@ -38,4 +43,94 @@
 	}
 
 	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 `<negative> <decimal> <grouping>`.  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
 }
--- /dev/null
+++ b/tpl/lang/lang_test.go
@@ -1,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)
+	}
+}