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} }`,