Skip to content

Commit 0e57e73

Browse files
Add regression fix for numeric string concatenation
1 parent c0f8257 commit 0e57e73

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

parser_expression.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pongo2
33
import (
44
"fmt"
55
"math"
6+
"strings"
67
)
78

89
type Expression struct {
@@ -251,6 +252,16 @@ func (expr *simpleExpression) Evaluate(ctx *ExecutionContext) (*Value, error) {
251252
}
252253
switch expr.opToken.Val {
253254
case "+":
255+
// If one operand is a number and the other is a numeric string,
256+
// treat both as numbers and do arithmetic (fixes issue #342).
257+
if (result.IsNumber() && t2.CanBeNumber()) || (t2.IsNumber() && result.CanBeNumber()) {
258+
if result.IsFloat() || t2.IsFloat() || (result.IsString() && strings.Contains(result.String(), ".")) || (t2.IsString() && strings.Contains(t2.String(), ".")) {
259+
// Result will be a float
260+
return AsValue(result.Float() + t2.Float()), nil
261+
}
262+
// Result will be an integer
263+
return AsValue(result.Integer() + t2.Integer()), nil
264+
}
254265
if result.IsString() || t2.IsString() {
255266
// Result will be a string
256267
return AsValue(result.String() + t2.String()), nil

pongo2_issues_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,87 @@ func TestIssue209(t *testing.T) {
416416
}
417417
}
418418

419+
func TestIssue342(t *testing.T) {
420+
// Test that adding a numeric string and a number results in arithmetic addition.
421+
// Bug: In v6, "10" + 5 returns "105" (string concatenation).
422+
// Expected: "10" + 5 should return 15 (arithmetic addition) as in v4.
423+
// See: https://github.com/flosch/pongo2/issues/342
424+
425+
tests := []struct {
426+
name string
427+
template string
428+
context pongo2.Context
429+
expected string
430+
}{
431+
{
432+
name: "numeric string + integer",
433+
template: "{{ a + b }}",
434+
context: pongo2.Context{"a": "10", "b": 5},
435+
expected: "15",
436+
},
437+
{
438+
name: "integer + numeric string",
439+
template: "{{ a + b }}",
440+
context: pongo2.Context{"a": 5, "b": "10"},
441+
expected: "15",
442+
},
443+
{
444+
name: "numeric string + float",
445+
template: "{{ a + b }}",
446+
context: pongo2.Context{"a": "10.5", "b": 2.5},
447+
expected: "13.000000",
448+
},
449+
{
450+
name: "float + numeric string",
451+
template: "{{ a + b }}",
452+
context: pongo2.Context{"a": 2.5, "b": "10.5"},
453+
expected: "13.000000",
454+
},
455+
{
456+
name: "non-numeric string + integer stays string concatenation",
457+
template: "{{ a + b }}",
458+
context: pongo2.Context{"a": "hello", "b": 5},
459+
expected: "hello5",
460+
},
461+
{
462+
name: "integer + non-numeric string stays string concatenation",
463+
template: "{{ a + b }}",
464+
context: pongo2.Context{"a": 5, "b": "hello"},
465+
expected: "5hello",
466+
},
467+
{
468+
name: "two strings remain concatenation",
469+
template: "{{ a + b }}",
470+
context: pongo2.Context{"a": "hello", "b": "world"},
471+
expected: "helloworld",
472+
},
473+
{
474+
name: "two numeric strings remain concatenation",
475+
template: "{{ a + b }}",
476+
context: pongo2.Context{"a": "10", "b": "5"},
477+
expected: "105",
478+
},
479+
}
480+
481+
for _, tt := range tests {
482+
t.Run(tt.name, func(t *testing.T) {
483+
tpl, err := pongo2.FromString(tt.template)
484+
if err != nil {
485+
t.Fatalf("failed to parse template: %v", err)
486+
}
487+
488+
result, err := tpl.Execute(tt.context)
489+
if err != nil {
490+
t.Fatalf("failed to execute template: %v", err)
491+
}
492+
493+
if result != tt.expected {
494+
t.Errorf("expected %q, got %q", tt.expected, result)
495+
}
496+
})
497+
}
498+
}
499+
419500
func TestIssue237(t *testing.T) {
420501
// Test that ifchanged with else clause works correctly when content doesn't change.
421502
// Bug: ifchanged without watched expressions would panic or not render else block

value.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ func (v *Value) IsNumber() bool {
8282
return v.IsInteger() || v.IsFloat()
8383
}
8484

85+
// CanBeNumber checks whether the value is either a number or a string
86+
// that can be parsed as a number.
87+
func (v *Value) CanBeNumber() bool {
88+
if v.IsNumber() {
89+
return true
90+
}
91+
if v.IsString() {
92+
_, err := strconv.ParseFloat(v.getResolvedValue().String(), 64)
93+
return err == nil
94+
}
95+
return false
96+
}
97+
8598
// IsTime checks whether the underlying value is a time.Time.
8699
func (v *Value) IsTime() bool {
87100
_, ok := v.Interface().(time.Time)

0 commit comments

Comments
 (0)