hugo/tpl/compare/compare.go

389 lines
9.8 KiB
Go
Raw Permalink Normal View History

// Copyright 2017 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.
// Package compare provides template functions for comparing values.
package compare
import (
"fmt"
"math"
"reflect"
"strconv"
"time"
"github.com/gohugoio/hugo/compare"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/types"
)
// New returns a new instance of the compare-namespaced template functions.
func New(loc *time.Location, caseInsensitive bool) *Namespace {
return &Namespace{loc: loc, caseInsensitive: caseInsensitive}
}
// Namespace provides template functions for the "compare" namespace.
type Namespace struct {
loc *time.Location
// Enable to do case insensitive string compares.
caseInsensitive bool
}
2022-12-21 07:11:08 -05:00
// Default checks whether a givenv is set and returns the default value defaultv if it
// is not. "Set" in this context means non-zero for numeric types and times;
// non-zero length for strings, arrays, slices, and maps;
// any boolean or struct value; or non-nil for any other types.
2022-12-21 07:11:08 -05:00
func (*Namespace) Default(defaultv any, givenv ...any) (any, error) {
// given is variadic because the following construct will not pass a piped
// argument when the key is missing: {{ index . "key" | default "foo" }}
2020-12-16 06:11:32 -05:00
// The Go template will complain that we got 1 argument when we expected 2.
2022-12-21 07:11:08 -05:00
if len(givenv) == 0 {
return defaultv, nil
}
2022-12-21 07:11:08 -05:00
if len(givenv) != 1 {
return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(givenv)+1)
}
2022-12-21 07:11:08 -05:00
g := reflect.ValueOf(givenv[0])
if !g.IsValid() {
2022-12-21 07:11:08 -05:00
return defaultv, nil
}
set := false
switch g.Kind() {
case reflect.Bool:
set = true
case reflect.String, reflect.Array, reflect.Slice, reflect.Map:
set = g.Len() != 0
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
set = g.Int() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
set = g.Uint() != 0
case reflect.Float32, reflect.Float64:
set = g.Float() != 0
case reflect.Complex64, reflect.Complex128:
set = g.Complex() != 0
case reflect.Struct:
2022-12-21 07:11:08 -05:00
switch actual := givenv[0].(type) {
case time.Time:
set = !actual.IsZero()
default:
set = true
}
default:
set = !g.IsNil()
}
if set {
2022-12-21 07:11:08 -05:00
return givenv[0], nil
}
2022-12-21 07:11:08 -05:00
return defaultv, nil
}
// Eq returns the boolean truth of arg1 == arg2 || arg1 == arg3 || arg1 == arg4.
func (n *Namespace) Eq(first any, others ...any) bool {
if n.caseInsensitive {
panic("caseInsensitive not implemented for Eq")
}
n.checkComparisonArgCount(1, others...)
normalize := func(v any) any {
if types.IsNil(v) {
return nil
}
if at, ok := v.(htime.AsTimeProvider); ok {
return at.AsTime(n.loc)
}
vv := reflect.ValueOf(v)
switch vv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return vv.Int()
case reflect.Float32, reflect.Float64:
return vv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i := vv.Uint()
// If it can fit in an int, convert it.
if i <= math.MaxInt64 {
return int64(i)
}
return i
Introduce a tree map for all content This commit introduces a new data structure to store pages and their resources. This data structure is backed by radix trees. This simplies tree operations, makes all pages a bundle, and paves the way for #6310. It also solves a set of annoying issues (see list below). Not a motivation behind this, but this commit also makes Hugo in general a little bit faster and more memory effective (see benchmarks). Especially for partial rebuilds on content edits, but also when taxonomies is in use. ``` name old time/op new time/op delta SiteNew/Bundle_with_image/Edit-16 1.32ms ± 8% 1.00ms ± 9% -24.42% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file/Edit-16 1.28ms ± 0% 0.94ms ± 0% -26.26% (p=0.029 n=4+4) SiteNew/Tags_and_categories/Edit-16 33.9ms ± 2% 21.8ms ± 1% -35.67% (p=0.029 n=4+4) SiteNew/Canonify_URLs/Edit-16 40.6ms ± 1% 37.7ms ± 3% -7.20% (p=0.029 n=4+4) SiteNew/Deep_content_tree/Edit-16 56.7ms ± 0% 51.7ms ± 1% -8.82% (p=0.029 n=4+4) SiteNew/Many_HTML_templates/Edit-16 19.9ms ± 2% 18.3ms ± 3% -7.64% (p=0.029 n=4+4) SiteNew/Page_collections/Edit-16 37.9ms ± 4% 34.0ms ± 2% -10.28% (p=0.029 n=4+4) SiteNew/Bundle_with_image-16 10.7ms ± 0% 10.6ms ± 0% -1.15% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 10.8ms ± 0% 10.7ms ± 0% -1.05% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 43.2ms ± 1% 39.6ms ± 1% -8.35% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 47.6ms ± 1% 47.3ms ± 0% ~ (p=0.057 n=4+4) SiteNew/Deep_content_tree-16 73.0ms ± 1% 74.2ms ± 1% ~ (p=0.114 n=4+4) SiteNew/Many_HTML_templates-16 37.9ms ± 0% 38.1ms ± 1% ~ (p=0.114 n=4+4) SiteNew/Page_collections-16 53.6ms ± 1% 54.7ms ± 1% +2.09% (p=0.029 n=4+4) name old alloc/op new alloc/op delta SiteNew/Bundle_with_image/Edit-16 486kB ± 0% 430kB ± 0% -11.47% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file/Edit-16 265kB ± 0% 209kB ± 0% -21.06% (p=0.029 n=4+4) SiteNew/Tags_and_categories/Edit-16 13.6MB ± 0% 8.8MB ± 0% -34.93% (p=0.029 n=4+4) SiteNew/Canonify_URLs/Edit-16 66.5MB ± 0% 63.9MB ± 0% -3.95% (p=0.029 n=4+4) SiteNew/Deep_content_tree/Edit-16 28.8MB ± 0% 25.8MB ± 0% -10.55% (p=0.029 n=4+4) SiteNew/Many_HTML_templates/Edit-16 6.16MB ± 0% 5.56MB ± 0% -9.86% (p=0.029 n=4+4) SiteNew/Page_collections/Edit-16 16.9MB ± 0% 16.0MB ± 0% -5.19% (p=0.029 n=4+4) SiteNew/Bundle_with_image-16 2.28MB ± 0% 2.29MB ± 0% +0.35% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 2.07MB ± 0% 2.07MB ± 0% ~ (p=0.114 n=4+4) SiteNew/Tags_and_categories-16 14.3MB ± 0% 13.2MB ± 0% -7.30% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 69.1MB ± 0% 69.0MB ± 0% ~ (p=0.343 n=4+4) SiteNew/Deep_content_tree-16 31.3MB ± 0% 31.8MB ± 0% +1.49% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 10.8MB ± 0% 10.9MB ± 0% +1.11% (p=0.029 n=4+4) SiteNew/Page_collections-16 21.4MB ± 0% 21.6MB ± 0% +1.15% (p=0.029 n=4+4) name old allocs/op new allocs/op delta SiteNew/Bundle_with_image/Edit-16 4.74k ± 0% 3.86k ± 0% -18.57% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file/Edit-16 4.73k ± 0% 3.85k ± 0% -18.58% (p=0.029 n=4+4) SiteNew/Tags_and_categories/Edit-16 301k ± 0% 198k ± 0% -34.14% (p=0.029 n=4+4) SiteNew/Canonify_URLs/Edit-16 389k ± 0% 373k ± 0% -4.07% (p=0.029 n=4+4) SiteNew/Deep_content_tree/Edit-16 338k ± 0% 262k ± 0% -22.63% (p=0.029 n=4+4) SiteNew/Many_HTML_templates/Edit-16 102k ± 0% 88k ± 0% -13.81% (p=0.029 n=4+4) SiteNew/Page_collections/Edit-16 176k ± 0% 152k ± 0% -13.32% (p=0.029 n=4+4) SiteNew/Bundle_with_image-16 26.8k ± 0% 26.8k ± 0% +0.05% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 26.8k ± 0% 26.8k ± 0% +0.05% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 273k ± 0% 245k ± 0% -10.36% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 396k ± 0% 398k ± 0% +0.39% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 317k ± 0% 325k ± 0% +2.53% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 146k ± 0% 147k ± 0% +0.98% (p=0.029 n=4+4) SiteNew/Page_collections-16 210k ± 0% 215k ± 0% +2.44% (p=0.029 n=4+4) ``` Fixes #6312 Fixes #6087 Fixes #6738 Fixes #6412 Fixes #6743 Fixes #6875 Fixes #6034 Fixes #6902 Fixes #6173 Fixes #6590
2019-09-10 05:26:34 -04:00
case reflect.String:
return vv.String()
default:
return v
}
}
normFirst := normalize(first)
for _, other := range others {
if e, ok := first.(compare.Eqer); ok {
if e.Eq(other) {
return true
}
continue
}
if e, ok := other.(compare.Eqer); ok {
if e.Eq(first) {
return true
}
continue
}
other = normalize(other)
if reflect.DeepEqual(normFirst, other) {
return true
}
}
return false
}
// Ne returns the boolean truth of arg1 != arg2 && arg1 != arg3 && arg1 != arg4.
func (n *Namespace) Ne(first any, others ...any) bool {
n.checkComparisonArgCount(1, others...)
for _, other := range others {
if n.Eq(first, other) {
return false
}
}
return true
}
// Ge returns the boolean truth of arg1 >= arg2 && arg1 >= arg3 && arg1 >= arg4.
func (n *Namespace) Ge(first any, others ...any) bool {
n.checkComparisonArgCount(1, others...)
for _, other := range others {
left, right := n.compareGet(first, other)
if !(left >= right) {
return false
}
}
return true
}
// Gt returns the boolean truth of arg1 > arg2 && arg1 > arg3 && arg1 > arg4.
func (n *Namespace) Gt(first any, others ...any) bool {
n.checkComparisonArgCount(1, others...)
for _, other := range others {
left, right := n.compareGet(first, other)
if !(left > right) {
return false
}
}
return true
}
// Le returns the boolean truth of arg1 <= arg2 && arg1 <= arg3 && arg1 <= arg4.
func (n *Namespace) Le(first any, others ...any) bool {
n.checkComparisonArgCount(1, others...)
for _, other := range others {
left, right := n.compareGet(first, other)
if !(left <= right) {
return false
}
}
return true
}
// LtCollate returns the boolean truth of arg1 < arg2 && arg1 < arg3 && arg1 < arg4.
// The provided collator will be used for string comparisons.
2022-04-21 04:59:13 -04:00
// This is for internal use.
func (n *Namespace) LtCollate(collator *langs.Collator, first any, others ...any) bool {
n.checkComparisonArgCount(1, others...)
for _, other := range others {
left, right := n.compareGetWithCollator(collator, first, other)
if !(left < right) {
return false
}
}
return true
}
// Lt returns the boolean truth of arg1 < arg2 && arg1 < arg3 && arg1 < arg4.
func (n *Namespace) Lt(first any, others ...any) bool {
return n.LtCollate(nil, first, others...)
}
func (n *Namespace) checkComparisonArgCount(min int, others ...any) bool {
if len(others) < min {
panic("missing arguments for comparison")
}
return true
}
// Conditional can be used as a ternary operator.
2022-12-21 07:11:08 -05:00
//
// It returns v1 if cond is true, else v2.
func (n *Namespace) Conditional(cond any, v1, v2 any) any {
if hreflect.IsTruthful(cond) {
2022-12-21 07:11:08 -05:00
return v1
}
2022-12-21 07:11:08 -05:00
return v2
}
func (ns *Namespace) compareGet(a any, b any) (float64, float64) {
return ns.compareGetWithCollator(nil, a, b)
}
func (ns *Namespace) compareTwoUints(a uint64, b uint64) (float64, float64) {
if a < b {
return 1, 0
} else if a == b {
return 0, 0
} else {
return 0, 1
}
}
func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b any) (float64, float64) {
if ac, ok := a.(compare.Comparer); ok {
c := ac.Compare(b)
if c < 0 {
return 1, 0
} else if c == 0 {
return 0, 0
} else {
return 0, 1
}
}
if bc, ok := b.(compare.Comparer); ok {
c := bc.Compare(a)
if c < 0 {
return 0, 1
} else if c == 0 {
return 0, 0
} else {
return 1, 0
}
}
var left, right float64
var leftStr, rightStr *string
av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
switch av.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
left = float64(av.Len())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if hreflect.IsUint(bv.Kind()) {
return ns.compareTwoUints(uint64(av.Int()), bv.Uint())
}
left = float64(av.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
left = float64(av.Uint())
case reflect.Uint64:
if hreflect.IsUint(bv.Kind()) {
return ns.compareTwoUints(av.Uint(), bv.Uint())
}
case reflect.Float32, reflect.Float64:
left = av.Float()
case reflect.String:
var err error
left, err = strconv.ParseFloat(av.String(), 64)
// Check if float is a special floating value and cast value as string.
if math.IsInf(left, 0) || math.IsNaN(left) || err != nil {
str := av.String()
leftStr = &str
}
case reflect.Struct:
if hreflect.IsTime(av.Type()) {
left = float64(ns.toTimeUnix(av))
}
2020-05-02 05:57:34 -04:00
case reflect.Bool:
left = 0
if av.Bool() {
left = 1
}
}
switch bv.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
right = float64(bv.Len())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if hreflect.IsUint(av.Kind()) {
return ns.compareTwoUints(av.Uint(), uint64(bv.Int()))
}
right = float64(bv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
right = float64(bv.Uint())
case reflect.Uint64:
if hreflect.IsUint(av.Kind()) {
return ns.compareTwoUints(av.Uint(), bv.Uint())
}
case reflect.Float32, reflect.Float64:
right = bv.Float()
case reflect.String:
var err error
right, err = strconv.ParseFloat(bv.String(), 64)
// Check if float is a special floating value and cast value as string.
if math.IsInf(right, 0) || math.IsNaN(right) || err != nil {
str := bv.String()
rightStr = &str
}
case reflect.Struct:
if hreflect.IsTime(bv.Type()) {
right = float64(ns.toTimeUnix(bv))
}
2020-05-02 05:57:34 -04:00
case reflect.Bool:
right = 0
if bv.Bool() {
right = 1
}
}
if (ns.caseInsensitive || collator != nil) && leftStr != nil && rightStr != nil {
var c int
if collator != nil {
c = collator.CompareStrings(*leftStr, *rightStr)
} else {
c = compare.Strings(*leftStr, *rightStr)
}
if c < 0 {
return 0, 1
} else if c > 0 {
return 1, 0
} else {
return 0, 0
}
}
switch {
case leftStr == nil || rightStr == nil:
case *leftStr < *rightStr:
return 0, 1
case *leftStr > *rightStr:
return 1, 0
default:
return 0, 0
}
return left, right
}
func (ns *Namespace) toTimeUnix(v reflect.Value) int64 {
t, ok := hreflect.AsTime(v, ns.loc)
if !ok {
panic("coding error: argument must be time.Time type reflect Value")
}
return t.Unix()
}