Add new operators has and not-has (#14841)

# Description
This PR add 2 new operators, `has` and `not-has`. They are basically
`in` and `not-in` with the order of operands swapped.

Motivation for this was the awkward way of searching for rows that
contain an item using `where`

```nushell
[[name, children]; [foo, [a, b, c]], [bar [d, e, f]]]
| where ("e" in $it.children)
```
vs
```nushell
[[name, children]; [foo, [a, b, c]], [bar [d, e, f]]]
| where children has "e"
``` 

# User-Facing Changes
Added `has` and `not-has` operators, mirroring `in` and `not-in`.

# Tests + Formatting

- 🟢 toolkit fmt
- 🟢 toolkit clippy
- 🟢 toolkit test
- 🟢 toolkit test stdlib

# After Submitting
This commit is contained in:
Bahex 2025-01-17 15:20:00 +03:00 committed by GitHub
parent 0587308684
commit 089c5221cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 41 deletions

View File

@ -107,10 +107,14 @@ impl Completer for OperatorCompletion {
("not-in", "Is not a member of (doesn't use regex)"),
],
Expr::FullCellPath(path) => match path.head.expr {
Expr::List(_) => vec![(
"++",
"Concatenates two lists, two strings, or two binary values",
)],
Expr::List(_) => vec![
(
"++",
"Concatenates two lists, two strings, or two binary values",
),
("has", "Contains a value of (doesn't use regex)"),
("not-has", "Does not contain a value of (doesn't use regex)"),
],
Expr::Var(id) => get_variable_completions(id, working_set),
_ => vec![],
},

View File

@ -141,6 +141,12 @@ fn description(operator: &Operator) -> &'static str {
Operator::Comparison(Comparison::NotIn) => {
"Checks if a value is not in a list, is not part of a string, or is not a key in a record."
}
Operator::Comparison(Comparison::Has) => {
"Checks if a list contains a value, a string contains another, or if a record has a key."
}
Operator::Comparison(Comparison::NotHas) => {
"Checks if a list does not contain a value, a string does not contain another, or if a record does not have a key."
}
Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.",
Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.",
Operator::Math(Math::Plus) => "Adds two values.",

View File

@ -187,3 +187,25 @@ fn where_gt_null() {
let actual = nu!("[{foo: 123} {}] | where foo? > 10 | to nuon");
assert_eq!(actual.out, "[[foo]; [123]]");
}
#[test]
fn has_operator() {
let actual = nu!(
r#"[[name, children]; [foo, [a, b]], [bar [b, c]], [baz, [c, d]]] | where children has "a" | to nuon"#
);
assert_eq!(actual.out, r#"[[name, children]; [foo, [a, b]]]"#);
let actual = nu!(
r#"[[name, children]; [foo, [a, b]], [bar [b, c]], [baz, [c, d]]] | where children not-has "a" | to nuon"#
);
assert_eq!(
actual.out,
r#"[[name, children]; [bar, [b, c]], [baz, [c, d]]]"#
);
let actual = nu!(r#"{foo: 1} has foo"#);
assert_eq!(actual.out, "true");
let actual = nu!(r#"{foo: 1} has bar "#);
assert_eq!(actual.out, "false");
}

View File

@ -952,6 +952,8 @@ fn binary_op(
}
Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?,
Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?,
Comparison::Has => lhs_val.has(op_span, &rhs_val, span)?,
Comparison::NotHas => lhs_val.not_has(op_span, &rhs_val, span)?,
Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?,
Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?,
},

View File

@ -5147,6 +5147,8 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
b"//" => Operator::Math(Math::FloorDivision),
b"in" => Operator::Comparison(Comparison::In),
b"not-in" => Operator::Comparison(Comparison::NotIn),
b"has" => Operator::Comparison(Comparison::Has),
b"not-has" => Operator::Comparison(Comparison::NotHas),
b"mod" => Operator::Math(Math::Modulo),
b"bit-or" => Operator::Bits(Bits::BitOr),
b"bit-xor" => Operator::Bits(Bits::BitXor),
@ -5187,7 +5189,7 @@ pub fn parse_operator(working_set: &mut StateWorkingSet, span: Span) -> Expressi
b"contains" => {
working_set.error(ParseError::UnknownOperator(
"contains",
"Did you mean '$string =~ $pattern' or '$element in $container'?",
"Did you mean 'has'?",
span,
));
return garbage(working_set, span);

View File

@ -821,7 +821,7 @@ pub fn math_result_type(
)
}
},
Operator::Comparison(Comparison::In) => match (&lhs.ty, &rhs.ty) {
Operator::Comparison(Comparison::In | Comparison::NotIn) => match (&lhs.ty, &rhs.ty) {
(t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None),
(Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None),
(Type::String, Type::String) => (Type::Bool, None),
@ -859,44 +859,48 @@ pub fn math_result_type(
)
}
},
Operator::Comparison(Comparison::NotIn) => match (&lhs.ty, &rhs.ty) {
(t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None),
(Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None),
(Type::String, Type::String) => (Type::Bool, None),
(Type::String, Type::Record(_)) => (Type::Bool, None),
Operator::Comparison(Comparison::Has | Comparison::NotHas) => {
let container = lhs;
let element = rhs;
match (&element.ty, &container.ty) {
(t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None),
(Type::Int | Type::Float | Type::Number, Type::Range) => (Type::Bool, None),
(Type::String, Type::String) => (Type::Bool, None),
(Type::String, Type::Record(_)) => (Type::Bool, None),
(Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None),
(Type::Custom(a), _) => (Type::Custom(a.clone()), None),
(Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None),
(Type::Custom(a), _) => (Type::Custom(a.clone()), None),
(Type::Any, _) => (Type::Bool, None),
(_, Type::Any) => (Type::Bool, None),
(Type::Int | Type::Float | Type::String, _) => {
*op = Expression::garbage(working_set, op.span);
(
Type::Any,
Some(ParseError::UnsupportedOperationRHS(
"subset comparison".into(),
op.span,
lhs.span,
lhs.ty.clone(),
rhs.span,
rhs.ty.clone(),
)),
)
(Type::Any, _) => (Type::Bool, None),
(_, Type::Any) => (Type::Bool, None),
(Type::Int | Type::Float | Type::String, _) => {
*op = Expression::garbage(working_set, op.span);
(
Type::Any,
Some(ParseError::UnsupportedOperationLHS(
"subset comparison".into(),
op.span,
element.span,
element.ty.clone(),
)),
)
}
_ => {
*op = Expression::garbage(working_set, op.span);
(
Type::Any,
Some(ParseError::UnsupportedOperationRHS(
"subset comparison".into(),
op.span,
element.span,
element.ty.clone(),
container.span,
container.ty.clone(),
)),
)
}
}
_ => {
*op = Expression::garbage(working_set, op.span);
(
Type::Any,
Some(ParseError::UnsupportedOperationLHS(
"subset comparison".into(),
op.span,
lhs.span,
lhs.ty.clone(),
)),
)
}
},
}
Operator::Bits(Bits::ShiftLeft)
| Operator::Bits(Bits::ShiftRight)
| Operator::Bits(Bits::BitOr)

View File

@ -17,6 +17,8 @@ pub enum Comparison {
NotRegexMatch,
In,
NotIn,
Has,
NotHas,
StartsWith,
EndsWith,
}
@ -90,6 +92,8 @@ impl Operator {
| Self::Comparison(Comparison::NotEqual)
| Self::Comparison(Comparison::In)
| Self::Comparison(Comparison::NotIn)
| Self::Comparison(Comparison::Has)
| Self::Comparison(Comparison::NotHas)
| Self::Math(Math::Concat) => 80,
Self::Bits(Bits::BitAnd) => 75,
Self::Bits(Bits::BitXor) => 70,
@ -123,6 +127,8 @@ impl Display for Operator {
Operator::Comparison(Comparison::EndsWith) => write!(f, "ends-with"),
Operator::Comparison(Comparison::In) => write!(f, "in"),
Operator::Comparison(Comparison::NotIn) => write!(f, "not-in"),
Operator::Comparison(Comparison::Has) => write!(f, "has"),
Operator::Comparison(Comparison::NotHas) => write!(f, "not-has"),
Operator::Math(Math::Plus) => write!(f, "+"),
Operator::Math(Math::Concat) => write!(f, "++"),
Operator::Math(Math::Minus) => write!(f, "-"),

View File

@ -256,6 +256,8 @@ pub trait Eval {
Comparison::NotEqual => lhs.ne(op_span, &rhs, expr_span),
Comparison::In => lhs.r#in(op_span, &rhs, expr_span),
Comparison::NotIn => lhs.not_in(op_span, &rhs, expr_span),
Comparison::Has => lhs.has(op_span, &rhs, expr_span),
Comparison::NotHas => lhs.not_has(op_span, &rhs, expr_span),
Comparison::StartsWith => lhs.starts_with(op_span, &rhs, expr_span),
Comparison::EndsWith => lhs.ends_with(op_span, &rhs, expr_span),
Comparison::RegexMatch => {

View File

@ -3540,6 +3540,14 @@ impl Value {
}
}
pub fn has(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
rhs.r#in(op, self, span)
}
pub fn not_has(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
rhs.r#not_in(op, self, span)
}
pub fn regex_match(
&self,
engine_state: &EngineState,