Skip to content

Commit be5d43d

Browse files
feat: make numeric-string arithmetic opt-in via Options flag
The "+" fix for issue #342 restored v4 behavior ("10" + 5 == 15), but that flips the v5/v6 string-concatenation behavior ("10" + 5 == "105"), which is itself a breaking change for current users. Gate the arithmetic path behind a new Options.NumericStringArithmetic flag (default false), so the v5/v6 behavior is preserved unless a caller explicitly opts in. This makes the fix non-breaking and mergeable. Add TestIssue342DefaultIsConcatenation to lock in the default behavior; existing TestIssue342 now enables the flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0e57e73 commit be5d43d

3 files changed

Lines changed: 58 additions & 5 deletions

File tree

options.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@ type Options struct {
1010
// If this is set to true leading spaces and tabs are stripped from the
1111
// start of a line to a block. Defaults to false
1212
LStripBlocks bool
13+
14+
// If this is set to true the "+" operator treats a numeric string and a
15+
// number as numbers and performs arithmetic addition (e.g. "10" + 5 == 15),
16+
// restoring pongo2 v4 behavior. Defaults to false, in which case "+"
17+
// performs string concatenation whenever either operand is a string (the
18+
// v5/v6 behavior, e.g. "10" + 5 == "105"). See issue #342.
19+
NumericStringArithmetic bool
1320
}
1421

1522
func newOptions() *Options {
1623
return &Options{
17-
TrimBlocks: false,
18-
LStripBlocks: false,
24+
TrimBlocks: false,
25+
LStripBlocks: false,
26+
NumericStringArithmetic: false,
1927
}
2028
}
2129

2230
// Update updates this options from another options.
2331
func (opt *Options) Update(other *Options) *Options {
2432
opt.TrimBlocks = other.TrimBlocks
2533
opt.LStripBlocks = other.LStripBlocks
34+
opt.NumericStringArithmetic = other.NumericStringArithmetic
2635

2736
return opt
2837
}

parser_expression.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ func (expr *simpleExpression) Evaluate(ctx *ExecutionContext) (*Value, error) {
254254
case "+":
255255
// If one operand is a number and the other is a numeric string,
256256
// treat both as numbers and do arithmetic (fixes issue #342).
257-
if (result.IsNumber() && t2.CanBeNumber()) || (t2.IsNumber() && result.CanBeNumber()) {
257+
// This is opt-in (restores pongo2 v4 behavior); when disabled the
258+
// default v5/v6 string-concatenation behavior below is used.
259+
if ctx.template.Options.NumericStringArithmetic &&
260+
((result.IsNumber() && t2.CanBeNumber()) || (t2.IsNumber() && result.CanBeNumber())) {
258261
if result.IsFloat() || t2.IsFloat() || (result.IsString() && strings.Contains(result.String(), ".")) || (t2.IsString() && strings.Contains(t2.String(), ".")) {
259262
// Result will be a float
260263
return AsValue(result.Float() + t2.Float()), nil

pongo2_issues_test.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,11 @@ func TestIssue209(t *testing.T) {
417417
}
418418

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

425427
tests := []struct {
@@ -485,6 +487,45 @@ func TestIssue342(t *testing.T) {
485487
t.Fatalf("failed to parse template: %v", err)
486488
}
487489

490+
// Opt in to the v4 numeric-string arithmetic behavior.
491+
tpl.Options.NumericStringArithmetic = true
492+
493+
result, err := tpl.Execute(tt.context)
494+
if err != nil {
495+
t.Fatalf("failed to execute template: %v", err)
496+
}
497+
498+
if result != tt.expected {
499+
t.Errorf("expected %q, got %q", tt.expected, result)
500+
}
501+
})
502+
}
503+
}
504+
505+
func TestIssue342DefaultIsConcatenation(t *testing.T) {
506+
// With the NumericStringArithmetic option left at its default (false), the
507+
// "+" operator keeps the v5/v6 string-concatenation behavior. This guards
508+
// against the fix re-introducing a breaking change for existing users.
509+
// See: https://github.com/flosch/pongo2/issues/342
510+
511+
tests := []struct {
512+
name string
513+
context pongo2.Context
514+
expected string
515+
}{
516+
{"numeric string + integer", pongo2.Context{"a": "10", "b": 5}, "105"},
517+
{"integer + numeric string", pongo2.Context{"a": 5, "b": "10"}, "510"},
518+
{"numeric string + float", pongo2.Context{"a": "10.5", "b": 2.5}, "10.52.500000"},
519+
{"non-numeric string + integer", pongo2.Context{"a": "hello", "b": 5}, "hello5"},
520+
}
521+
522+
for _, tt := range tests {
523+
t.Run(tt.name, func(t *testing.T) {
524+
tpl, err := pongo2.FromString("{{ a + b }}")
525+
if err != nil {
526+
t.Fatalf("failed to parse template: %v", err)
527+
}
528+
488529
result, err := tpl.Execute(tt.context)
489530
if err != nil {
490531
t.Fatalf("failed to execute template: %v", err)

0 commit comments

Comments
 (0)