Skip to content
Open
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
13 changes: 11 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ type Options struct {
// If this is set to true leading spaces and tabs are stripped from the
// start of a line to a block. Defaults to false
LStripBlocks bool

// If this is set to true the "+" operator treats a numeric string and a
// number as numbers and performs arithmetic addition (e.g. "10" + 5 == 15),
// restoring pongo2 v4 behavior. Defaults to false, in which case "+"
// performs string concatenation whenever either operand is a string (the
// v5/v6 behavior, e.g. "10" + 5 == "105"). See issue #342.
NumericStringArithmetic bool
}

func newOptions() *Options {
return &Options{
TrimBlocks: false,
LStripBlocks: false,
TrimBlocks: false,
LStripBlocks: false,
NumericStringArithmetic: false,
}
}

// Update updates this options from another options.
func (opt *Options) Update(other *Options) *Options {
opt.TrimBlocks = other.TrimBlocks
opt.LStripBlocks = other.LStripBlocks
opt.NumericStringArithmetic = other.NumericStringArithmetic

return opt
}
14 changes: 14 additions & 0 deletions parser_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pongo2
import (
"fmt"
"math"
"strings"
)

type Expression struct {
Expand Down Expand Up @@ -251,6 +252,19 @@ func (expr *simpleExpression) Evaluate(ctx *ExecutionContext) (*Value, error) {
}
switch expr.opToken.Val {
case "+":
// If one operand is a number and the other is a numeric string,
// treat both as numbers and do arithmetic (fixes issue #342).
// This is opt-in (restores pongo2 v4 behavior); when disabled the
// default v5/v6 string-concatenation behavior below is used.
if ctx.template.Options.NumericStringArithmetic &&
((result.IsNumber() && t2.CanBeNumber()) || (t2.IsNumber() && result.CanBeNumber())) {
if result.IsFloat() || t2.IsFloat() || (result.IsString() && strings.Contains(result.String(), ".")) || (t2.IsString() && strings.Contains(t2.String(), ".")) {
// Result will be a float
return AsValue(result.Float() + t2.Float()), nil
}
// Result will be an integer
return AsValue(result.Integer() + t2.Integer()), nil
}
if result.IsString() || t2.IsString() {
// Result will be a string
return AsValue(result.String() + t2.String()), nil
Expand Down
122 changes: 122 additions & 0 deletions pongo2_issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,128 @@ func TestIssue209(t *testing.T) {
}
}

func TestIssue342(t *testing.T) {
// Test that, with the opt-in NumericStringArithmetic option enabled, adding
// a numeric string and a number results in arithmetic addition.
// Bug: In v6, "10" + 5 returns "105" (string concatenation).
// With the option on, "10" + 5 returns 15 (arithmetic addition) as in v4.
// The option is off by default; see TestIssue342DefaultIsConcatenation.
// See: https://github.com/flosch/pongo2/issues/342

tests := []struct {
name string
template string
context pongo2.Context
expected string
}{
{
name: "numeric string + integer",
template: "{{ a + b }}",
context: pongo2.Context{"a": "10", "b": 5},
expected: "15",
},
{
name: "integer + numeric string",
template: "{{ a + b }}",
context: pongo2.Context{"a": 5, "b": "10"},
expected: "15",
},
{
name: "numeric string + float",
template: "{{ a + b }}",
context: pongo2.Context{"a": "10.5", "b": 2.5},
expected: "13.000000",
},
{
name: "float + numeric string",
template: "{{ a + b }}",
context: pongo2.Context{"a": 2.5, "b": "10.5"},
expected: "13.000000",
},
{
name: "non-numeric string + integer stays string concatenation",
template: "{{ a + b }}",
context: pongo2.Context{"a": "hello", "b": 5},
expected: "hello5",
},
{
name: "integer + non-numeric string stays string concatenation",
template: "{{ a + b }}",
context: pongo2.Context{"a": 5, "b": "hello"},
expected: "5hello",
},
{
name: "two strings remain concatenation",
template: "{{ a + b }}",
context: pongo2.Context{"a": "hello", "b": "world"},
expected: "helloworld",
},
{
name: "two numeric strings remain concatenation",
template: "{{ a + b }}",
context: pongo2.Context{"a": "10", "b": "5"},
expected: "105",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tpl, err := pongo2.FromString(tt.template)
if err != nil {
t.Fatalf("failed to parse template: %v", err)
}

// Opt in to the v4 numeric-string arithmetic behavior.
tpl.Options.NumericStringArithmetic = true

result, err := tpl.Execute(tt.context)
if err != nil {
t.Fatalf("failed to execute template: %v", err)
}

if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}

func TestIssue342DefaultIsConcatenation(t *testing.T) {
// With the NumericStringArithmetic option left at its default (false), the
// "+" operator keeps the v5/v6 string-concatenation behavior. This guards
// against the fix re-introducing a breaking change for existing users.
// See: https://github.com/flosch/pongo2/issues/342

tests := []struct {
name string
context pongo2.Context
expected string
}{
{"numeric string + integer", pongo2.Context{"a": "10", "b": 5}, "105"},
{"integer + numeric string", pongo2.Context{"a": 5, "b": "10"}, "510"},
{"numeric string + float", pongo2.Context{"a": "10.5", "b": 2.5}, "10.52.500000"},
{"non-numeric string + integer", pongo2.Context{"a": "hello", "b": 5}, "hello5"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tpl, err := pongo2.FromString("{{ a + b }}")
if err != nil {
t.Fatalf("failed to parse template: %v", err)
}

result, err := tpl.Execute(tt.context)
if err != nil {
t.Fatalf("failed to execute template: %v", err)
}

if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}

func TestIssue237(t *testing.T) {
// Test that ifchanged with else clause works correctly when content doesn't change.
// Bug: ifchanged without watched expressions would panic or not render else block
Expand Down
13 changes: 13 additions & 0 deletions value.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ func (v *Value) IsNumber() bool {
return v.IsInteger() || v.IsFloat()
}

// CanBeNumber checks whether the value is either a number or a string
// that can be parsed as a number.
func (v *Value) CanBeNumber() bool {
if v.IsNumber() {
return true
}
if v.IsString() {
_, err := strconv.ParseFloat(v.getResolvedValue().String(), 64)
return err == nil
}
return false
}

// IsTime checks whether the underlying value is a time.Time.
func (v *Value) IsTime() bool {
_, ok := v.Interface().(time.Time)
Expand Down