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
11 changes: 11 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,16 @@ 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).
if (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
81 changes: 81 additions & 0 deletions pongo2_issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,87 @@ func TestIssue209(t *testing.T) {
}
}

func TestIssue342(t *testing.T) {
// Test that adding a numeric string and a number results in arithmetic addition.
// Bug: In v6, "10" + 5 returns "105" (string concatenation).
// Expected: "10" + 5 should return 15 (arithmetic addition) as in v4.
// 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)
}

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