Skip to content

Commit dca94bd

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 ae8deb2 commit dca94bd

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
@@ -408,9 +408,11 @@ func TestIssue209(t *testing.T) {
408408
}
409409

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

416418
tests := []struct {
@@ -476,6 +478,45 @@ func TestIssue342(t *testing.T) {
476478
t.Fatalf("failed to parse template: %v", err)
477479
}
478480

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

0 commit comments

Comments
 (0)