Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add template func numFormat #1450

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/content/templates/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,29 @@ e.g.

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

### numFormat

`numFormat` allows you to format integers and floats with a custom thousands seperator and a
decimal mark. Furthermore, you can define the precision after the comma, but **only** up to 9 digits! Required are the expected format of the output and the number itself. Let's look at some examples:

```
<!-- "" is the default format -->
{{ numFormat "" 12345.6789 }}
<!-- outputs 12,345.68 -->

{{ numFormat "#,###.##" 12345.6789 }}
<!-- outputs 12,345.68 -->

{{ numFormat "#,###." 12345.6789 }}
<!-- outputs 12,346 -->

{{ numFormat "#,###" 12345.6789 }}
<!-- outputs 12345,679 -->

{{ numFormat "#.###,######" 12345.6789 }}
<!-- outputs 12.345,678900 -->
```

## Strings

### chomp
Expand Down
12 changes: 12 additions & 0 deletions tpl/template_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,17 @@ func sha1(in interface{}) (string, error) {
return hex.EncodeToString(hash[:]), nil
}

// numFormat can format floats and integers with custom thousands seperators
// and decimal seperators.
func numFormat(format string, num interface{}) (string, error) {
switch conv := num.(type) {
case int, int8, int16, int32, int64, float32, float64:
return renderNumber(format, conv.(float64))
default:
return "", fmt.Errorf("%T is an invalid type and can't be formatted.", num)
}
}

func init() {
funcMap = template.FuncMap{
"absURL": func(a string) template.HTML { return template.HTML(helpers.AbsURL(a)) },
Expand All @@ -1748,6 +1759,7 @@ func init() {
"eq": eq,
"findRE": findRE,
"first": first,
"numFormat": numFormat,
"ge": ge,
"getCSV": getCSV,
"getJSON": getJSON,
Expand Down
29 changes: 29 additions & 0 deletions tpl/template_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ div: {{div 6 3}}
emojify: {{ "I :heart: Hugo" | emojify }}
eq: {{ if eq .Section "blog" }}current{{ end }}
findRE: {{ findRE "[G|g]o" "Hugo is a static side generator written in Go." 1 }}
numFormat: {{ numFormat "#,###" 12345.6789 }}
hasPrefix 1: {{ hasPrefix "Hugo" "Hu" }}
hasPrefix 2: {{ hasPrefix "Hugo" "Fu" }}
in: {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }}
Expand Down Expand Up @@ -141,6 +142,7 @@ div: 2
emojify: I ❤️ Hugo
eq: current
findRE: [go]
numFormat: 12345,679
hasPrefix 1: true
hasPrefix 2: false
in: Substring found!
Expand Down Expand Up @@ -2287,3 +2289,30 @@ func TestReadFile(t *testing.T) {
}
}
}

func TestNumFormat(t *testing.T) {
testNum := 12345.6789

for _, this := range []struct {
format string
expect string
}{
{"", "12,345.68"},
{"#,###.##", "12,345.68"},
{"#,###.", "12,346"},
{"#,###", "12345,679"},
{"#.###,######", "12.345,678900"},
} {
formNum, err := numFormat(this.format, testNum)

if err != nil {
t.Errorf("Formating %v with [%s] as format caused an error: %s",
testNum, this.format, err)
}

if formNum != this.expect {
t.Errorf("Tried to format %v with [%s]: got %s - expected %v",
testNum, this.format, formNum, this.expect)
}
}
}
176 changes: 176 additions & 0 deletions tpl/template_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Integer and float rendering
//
// Author: https://github.com/gorhill
// Source: https://gist.github.com/gorhill/5285193
//
// Released under the DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE.

package tpl

import (
"errors"
"fmt"
"math"
"strconv"
)

var renderFloatPrecisionMultipliers = [10]float64{
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
}

var renderFloatPrecisionRounders = [10]float64{
0.5,
0.05,
0.005,
0.0005,
0.00005,
0.000005,
0.0000005,
0.00000005,
0.000000005,
0.0000000005,
}

func renderNumber(format string, n float64) (string, error) {
// Special cases:
// NaN = "NaN"
// +Inf = "+Infinity"
// -Inf = "-Infinity"
if math.IsNaN(n) {
return "", fmt.Errorf("%s is not a number", n)
}
if n > math.MaxFloat64 {
return "", fmt.Errorf("%s needs to be a smaller positive number.", n)
}
if n < -math.MaxFloat64 {
return "", fmt.Errorf("%s needs to be a greater negative number.", n)
}

// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
negativeStr := "-"

if len(format) > 0 {
// If there is an explicit format directive,
// then default values are these:
precision = 9
thousandStr = ""

// collect indices of meaningful formatting directives
formatDirectiveChars := []rune(format)
formatDirectiveIndices := make([]int, 0)
for i, char := range formatDirectiveChars {
if char != '#' && char != '0' {
formatDirectiveIndices = append(formatDirectiveIndices, i)
}
}

if len(formatDirectiveIndices) > 0 {
// Directive at index 0:
// Must be a '+'
// Raise an error if not the case
// index: 0123456789
// +0.000,000
// +000,000.0
// +0000.00
// +0000
if formatDirectiveIndices[0] == 0 {
if formatDirectiveChars[formatDirectiveIndices[0]] != '+' {
return "", fmt.Errorf("Invalid positive sign directive of %s.", n)
}
positiveStr = "+"
formatDirectiveIndices = formatDirectiveIndices[1:]
}

// Two directives:
// First is thousands separator
// Raise an error if not followed by 3-digit
// 0123456789
// 0.000,000
// 000,000.00
if len(formatDirectiveIndices) == 2 {
if (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 {
return "", errors.New("Thousands separator directive must be followed by 3 digit-specifiers.")
}
thousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]])
formatDirectiveIndices = formatDirectiveIndices[1:]
}

// One directive:
// Directive is decimal separator
// The number of digit-specifier following the separator indicates wanted precision
// 0123456789
// 0.00
// 000,0000
if len(formatDirectiveIndices) == 1 {
decimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]])
precision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1
}
}
}

// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}

// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])

// generate integer part string
intStr := strconv.Itoa(int(intf))

// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}

// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr, nil
}

// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}

return signStr + intStr + decimalStr + fracStr, nil
}