tpl/lang: Add NumFmt function
authorCameron Moore <moorereason@gmail.com>
Thu, 29 Dec 2016 04:09:31 +0000 (22:09 -0600)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 18 May 2017 06:49:20 +0000 (09:49 +0300)
NumFmt formats a number with a given precision using the requested
decimal, grouping, and negative characters.

Fixes #1444

docs/content/templates/functions.md
tpl/lang/init.go
tpl/lang/lang.go
tpl/lang/lang_test.go [new file with mode: 0644]

index c1fd8ebad7a72326014473a730f7109a323afecf..c514a4ec84c437ea1047b2ae67dffb4a39e08a74 100644 (file)
@@ -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 `<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
index 6cf8e790dbd329fbd9694919580adcd5f80b5601..ea67afab0280293195ef866017c20c5b329819c4 100644 (file)
@@ -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
 
        }
index c84728f3b4dfcd6168472373150255e58b2fafaf..45a640d7a3e89e46a824192d07762676f061118f 100644 (file)
 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 `<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
+}
diff --git a/tpl/lang/lang_test.go b/tpl/lang/lang_test.go
new file mode 100644 (file)
index 0000000..45ed506
--- /dev/null
@@ -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)
+       }
+}