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
This commit is contained in:
dualfroz committed 2026-09-03 13:29:22 +02:00
1 parent b9be9a6184
commit 699d0e35c0
3 files changed
+86 -1

No files matched your search

+20
View File
@@ -93,6 +93,26 @@ false
true 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 ## Match nulls
Running Running
```bash ```bash
+17 -1
View File
@@ -5,6 +5,22 @@ func equalsOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
return crossFunction(d, context, expressionNode, isEquals(false), true) 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) { 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) { return func(_ *dataTreeNavigator, _ Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
value := false value := false
@@ -32,7 +48,7 @@ func isEquals(flip bool) func(d *dataTreeNavigator, context Context, lhs *Candid
if lhs.Tag == "!!null" { if lhs.Tag == "!!null" {
value = (rhs.Tag == "!!null") value = (rhs.Tag == "!!null")
} else if lhs.Kind == ScalarNode && rhs.Kind == ScalarNode { } 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) log.Debugf("%v == %v ? %v", NodeToString(lhs), NodeToString(rhs), value)
if flip { if flip {
+49
View File
@@ -146,6 +146,55 @@ var equalsOperatorScenarios = []expressionScenario{
"D0, P[2], (!!bool)::true\n", "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, skipDoc: true,
document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`, document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`,