From 699d0e35c054594f655681d2508d29580bac1d32 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 3 Sep 2026 13:29:22 +0200 Subject: [PATCH] fix(equals): compare integers by value, not by raw text The == and != operators matched the raw text of two scalars, so the same integer written in a different base never matched: `0x10 == 16`, `0o644 == 420` and `1_000 == 1000` were all false. This contradicted yq's own comparison operators, which parse those formats and agree the values are equal - for `0o644`, both `> 419` and `< 421` are true, yet `== 420` was false. It also contradicted the documented "Match number" behaviour of `==`. Give ==/!= the same !!int/!!int branch the comparison operators already have: parse both sides with the existing parseInt64 helper and compare the resulting int64s. If either side does not fit in an int64, fall back to the previous raw match rather than raising a new error. String globbing, floats and null handling are unchanged. Fixes #1977 --- pkg/yqlib/doc/operators/equals.md | 20 +++++++++++++ pkg/yqlib/operator_equals.go | 18 +++++++++++- pkg/yqlib/operator_equals_test.go | 49 +++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/pkg/yqlib/doc/operators/equals.md b/pkg/yqlib/doc/operators/equals.md index ca5099b..77a7b5f 100644 --- a/pkg/yqlib/doc/operators/equals.md +++ b/pkg/yqlib/doc/operators/equals.md @@ -93,6 +93,26 @@ false true ``` +## Match number written in a different base +Integers are compared by value, so the hexadecimal and octal forms of 16 match the decimal form. + +Given a sample.yml file of: +```yaml +- 0x10 +- 0o20 +- 16 +``` +then +```bash +yq '.[] | (. == 16)' sample.yml +``` +will output +```yaml +true +true +true +``` + ## Match nulls Running ```bash diff --git a/pkg/yqlib/operator_equals.go b/pkg/yqlib/operator_equals.go index 4bcf28d..04babe5 100644 --- a/pkg/yqlib/operator_equals.go +++ b/pkg/yqlib/operator_equals.go @@ -5,6 +5,22 @@ func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *Expre return crossFunction(d, context, expressionNode, isEquals(false), true) } +// scalarsMatch compares two scalar nodes. Integers are compared by value, so +// that the same number written in a different base (e.g. 0x10 and 16) matches, +// like the comparison operators already do. Anything else - including integers +// too big for an int64 - falls back to matching the raw values. +func scalarsMatch(lhs *CandidateNode, rhs *CandidateNode) bool { + if lhs.guessTagFromCustomType() == "!!int" && rhs.guessTagFromCustomType() == "!!int" { + _, lhsNum, lhsErr := parseInt64(lhs.Value) + _, rhsNum, rhsErr := parseInt64(rhs.Value) + if lhsErr == nil && rhsErr == nil { + return lhsNum == rhsNum + } + log.Debugf("could not parse %v and/or %v as int64, comparing the raw values", lhs.Value, rhs.Value) + } + return matchKey(lhs.Value, rhs.Value) +} + func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { return func(_ *dataTreeNavigator, _ Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { value := false @@ -32,7 +48,7 @@ func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *Candid if lhs.Tag == "!!null" { value = (rhs.Tag == "!!null") } else if lhs.Kind == ScalarNode && rhs.Kind == ScalarNode { - value = matchKey(lhs.Value, rhs.Value) + value = scalarsMatch(lhs, rhs) } log.Debugf("%v == %v ? %v", NodeToString(lhs), NodeToString(rhs), value) if flip { diff --git a/pkg/yqlib/operator_equals_test.go b/pkg/yqlib/operator_equals_test.go index 43a2fa8..7b74642 100644 --- a/pkg/yqlib/operator_equals_test.go +++ b/pkg/yqlib/operator_equals_test.go @@ -146,6 +146,55 @@ var equalsOperatorScenarios = []expressionScenario{ "D0, P[2], (!!bool)::true\n", }, }, + { + description: "Match number written in a different base", + subdescription: "Integers are compared by value, so the hexadecimal and octal forms of 16 match the decimal form.", + document: `[0x10, 0o20, 16]`, + expression: `.[] | (. == 16)`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + "D0, P[1], (!!bool)::true\n", + "D0, P[2], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: `[0x10, 0o20, 16]`, + expression: `.[] | (. != 16)`, + expected: []string{ + "D0, P[0], (!!bool)::false\n", + "D0, P[1], (!!bool)::false\n", + "D0, P[2], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `[1_000, 1000]`, + expression: `.[] | (. == 1000)`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + "D0, P[1], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: `[0x10, 17]`, + expression: `.[] | (. == 16)`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + "D0, P[1], (!!bool)::false\n", + }, + }, + { + // integers that don't fit in an int64 can't be compared numerically, + // fall back to matching the raw values. + skipDoc: true, + document: `[9223372036854775808, 9223372036854775808]`, + expression: `.[0] == .[1]`, + expected: []string{ + "D0, P[0], (!!bool)::true\n", + }, + }, { skipDoc: true, document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`,