diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 1a44d6fffb..16572d8ad8 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -190,6 +190,8 @@ pub fn eval_expression( Operator::GreaterThanOrEqual => lhs.gte(op_span, &rhs), Operator::Equal => lhs.eq(op_span, &rhs), Operator::NotEqual => lhs.ne(op_span, &rhs), + Operator::In => lhs.r#in(op_span, &rhs), + Operator::NotIn => lhs.not_in(op_span, &rhs), x => Err(ShellError::UnsupportedOperator(x, op_span)), } } diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 03ce14c6e6..ca087d0a87 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -273,6 +273,50 @@ pub fn math_result_type( ) } }, + Operator::In => match (&lhs.ty, &rhs.ty) { + (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), + (Type::Int | Type::Float, Type::Range) => (Type::Bool, None), + (Type::String, Type::String) => (Type::Bool, None), + (Type::String, Type::Record(_, _)) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::NotIn => match (&lhs.ty, &rhs.ty) { + (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), + (Type::Int | Type::Float, Type::Range) => (Type::Bool, None), + (Type::String, Type::String) => (Type::Bool, None), + (Type::String, Type::Record(_, _)) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, _ => { *op = Expression::garbage(op.span); diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 334790ab27..f767f80e2b 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; pub use stream::*; pub use unit::*; -use std::fmt::Debug; +use std::{cmp::Ordering, fmt::Debug}; use crate::ast::{CellPath, PathMember}; use crate::{span, BlockId, Span, Type}; @@ -441,96 +441,83 @@ impl Value { } } +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + // Compare two floating point numbers. The decision interval for equality is dynamically + // scaled as the value being compared increases in magnitude. + fn compare_floats(val: f64, other: f64) -> Option { + let prec = f64::EPSILON.max(val.abs() * f64::EPSILON); + + if (other - val).abs() < prec { + return Some(Ordering::Equal); + } + + val.partial_cmp(&other) + } + + match (self, other) { + (Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => lhs.partial_cmp(rhs), + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => lhs.partial_cmp(rhs), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + compare_floats(*lhs, *rhs) + } + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + compare_floats(*lhs as f64, *rhs) + } + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + compare_floats(*lhs, *rhs as f64) + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Block { val: b1, .. }, Value::Block { val: b2, .. }) if b1 == b2 => { + Some(Ordering::Equal) + } + (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => lhs.partial_cmp(rhs), + ( + Value::Record { + vals: lhs, + cols: lhs_headers, + .. + }, + Value::Record { + vals: rhs, + cols: rhs_headers, + .. + }, + ) if lhs_headers == rhs_headers && lhs == rhs => Some(Ordering::Equal), + (Value::Stream { stream: lhs, .. }, Value::Stream { stream: rhs, .. }) => { + lhs.clone().partial_cmp(rhs.clone()) + } + (Value::Stream { stream: lhs, .. }, Value::String { val: rhs, .. }) => { + lhs.clone().collect_string().partial_cmp(rhs) + } + (Value::String { val: lhs, .. }, Value::Stream { stream: rhs, .. }) => { + lhs.partial_cmp(&rhs.clone().collect_string()) + } + // NOTE: This may look a bit strange, but a `Stream` is still just a `List`, it just + // happens to be in an iterator form instead of a concrete form. The contained values + // can be compared. + (Value::Stream { stream: lhs, .. }, Value::List { vals: rhs, .. }) => { + lhs.clone().collect::>().partial_cmp(rhs) + } + (Value::List { vals: lhs, .. }, Value::Stream { stream: rhs, .. }) => { + lhs.partial_cmp(&rhs.clone().collect::>()) + } + (_, _) => None, + } + } +} + impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => lhs == rhs, - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => lhs == rhs, - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => lhs == rhs, - (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => lhs == rhs, - (Value::Block { val: b1, .. }, Value::Block { val: b2, .. }) => b1 == b2, - (Value::List { vals: vals_lhs, .. }, Value::List { vals: vals_rhs, .. }) => { - for (lhs, rhs) in vals_lhs.iter().zip(vals_rhs) { - if lhs != rhs { - return false; - } - } - - true - } - ( - Value::Record { - cols: cols_lhs, - vals: vals_lhs, - .. - }, - Value::Record { - cols: cols_rhs, - vals: vals_rhs, - .. - }, - ) => { - if cols_lhs != cols_rhs { - return false; - } - - for (lhs, rhs) in vals_lhs.iter().zip(vals_rhs) { - if lhs != rhs { - return false; - } - } - - true - } - ( - Value::Stream { - stream: stream_lhs, .. - }, - Value::Stream { - stream: stream_rhs, .. - }, - ) => { - let vals_lhs: Vec = stream_lhs.clone().collect(); - let vals_rhs: Vec = stream_rhs.clone().collect(); - - vals_lhs == vals_rhs - } - // Note: This may look a bit strange, but a Stream is still just a List, - // it just happens to be in an iterator form instead of a concrete form. If the contained - // values are the same then it should be treated as equal - ( - Value::Stream { - stream: stream_lhs, .. - }, - Value::List { - vals: stream_rhs, .. - }, - ) => { - let vals_lhs: Vec = stream_lhs.clone().collect(); - let vals_rhs: Vec = - stream_rhs.clone().into_iter().into_value_stream().collect(); - - vals_lhs == vals_rhs - } - // Note: This may look a bit strange, but a Stream is still just a List, - // it just happens to be in an iterator form instead of a concrete form. If the contained - // values are the same then it should be treated as equal - ( - Value::List { - vals: stream_lhs, .. - }, - Value::Stream { - stream: stream_rhs, .. - }, - ) => { - let vals_lhs: Vec = - stream_lhs.clone().into_iter().into_value_stream().collect(); - let vals_rhs: Vec = stream_rhs.clone().collect(); - - vals_lhs == vals_rhs - } - _ => false, - } + self.partial_cmp(other).map_or(false, Ordering::is_eq) } } @@ -717,37 +704,12 @@ impl Value { pub fn lt(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs < rhs, + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Less), span, }), - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) < *rhs, - span, - }), - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs < *rhs as f64, - span, - }), - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs < rhs, - span, - }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs < rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs < rhs, - span, - }) - } - - _ => Err(ShellError::OperatorMismatch { + None => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), lhs_span: self.span(), @@ -759,36 +721,12 @@ impl Value { pub fn lte(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs <= rhs, + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Less | Ordering::Equal), span, }), - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) <= *rhs, - span, - }), - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs <= *rhs as f64, - span, - }), - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs <= rhs, - span, - }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs <= rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs <= rhs, - span, - }) - } - _ => Err(ShellError::OperatorMismatch { + None => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), lhs_span: self.span(), @@ -800,36 +738,12 @@ impl Value { pub fn gt(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs > rhs, + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Greater), span, }), - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) > *rhs, - span, - }), - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs > *rhs as f64, - span, - }), - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs > rhs, - span, - }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs > rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs > rhs, - span, - }) - } - _ => Err(ShellError::OperatorMismatch { + None => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), lhs_span: self.span(), @@ -841,36 +755,12 @@ impl Value { pub fn gte(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs >= rhs, + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Greater | Ordering::Equal), span, }), - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) >= *rhs, - span, - }), - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs >= *rhs as f64, - span, - }), - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs >= rhs, - span, - }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs >= rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs >= rhs, - span, - }) - } - _ => Err(ShellError::OperatorMismatch { + None => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), lhs_span: self.span(), @@ -882,62 +772,12 @@ impl Value { pub fn eq(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); - match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs == rhs, + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Equal), span, }), - (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { - val: lhs == rhs, - span, - }), - // FIXME: these should consider machine epsilon - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) == *rhs, - span, - }), - // FIXME: these should consider machine epsilon - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs == *rhs as f64, - span, - }), - // FIXME: these should consider machine epsilon - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs == rhs, - span, - }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs == rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs == rhs, - span, - }) - } - (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => Ok(Value::Bool { - val: lhs == rhs, - span, - }), - ( - Value::Record { - vals: lhs, - cols: lhs_headers, - .. - }, - Value::Record { - vals: rhs, - cols: rhs_headers, - .. - }, - ) => Ok(Value::Bool { - val: lhs_headers == rhs_headers && lhs == rhs, - span, - }), - _ => Err(ShellError::OperatorMismatch { + None => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), lhs_span: self.span(), @@ -949,62 +789,79 @@ impl Value { pub fn ne(&self, op: Span, rhs: &Value) -> Result { let span = span(&[self.span(), rhs.span()]); + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: !matches!(ordering, Ordering::Equal), + span, + }), + None => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span(), + rhs_ty: rhs.get_type(), + rhs_span: rhs.span(), + }), + } + } + + pub fn r#in(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span(), rhs.span()]); + match (self, rhs) { - (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: lhs != rhs, + (lhs, Value::Range { val: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), span, }), (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { - val: lhs != rhs, + val: rhs.contains(lhs), span, }), - // FIXME: these should consider machine epsilon - (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: (*lhs as f64) != *rhs, + (lhs, Value::List { vals: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), span, }), - // FIXME: these should consider machine epsilon - (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Bool { - val: *lhs != *rhs as f64, + (Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), span, }), - // FIXME: these should consider machine epsilon - (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Bool { - val: lhs != rhs, + (lhs, Value::Stream { stream: rhs, .. }) => Ok(Value::Bool { + val: rhs.clone().any(|x| lhs == &x), span, }), - (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs != rhs, - span, - }) - } - (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { - Ok(Value::Bool { - val: lhs != rhs, - span, - }) - } - (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => Ok(Value::Bool { - val: lhs != rhs, - span, - }), - ( - Value::Record { - vals: lhs, - cols: lhs_headers, - .. - }, - Value::Record { - vals: rhs, - cols: rhs_headers, - .. - }, - ) => Ok(Value::Bool { - val: lhs_headers != rhs_headers || lhs != rhs, - span, + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span(), + rhs_ty: rhs.get_type(), + rhs_span: rhs.span(), }), + } + } + pub fn not_in(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span(), rhs.span()]); + + match (self, rhs) { + (lhs, Value::Range { val: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (lhs, Value::List { vals: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (lhs, Value::Stream { stream: rhs, .. }) => Ok(Value::Bool { + val: rhs.clone().all(|x| lhs != &x), + span, + }), _ => Err(ShellError::OperatorMismatch { op_span: op, lhs_ty: self.get_type(), diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 369ff03e87..8d2bbbb811 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -103,6 +103,25 @@ impl Range { inclusion: operator.inclusion, }) } + + #[inline] + fn moves_up(&self) -> bool { + self.from <= self.to + } + + #[inline] + fn is_end_inclusive(&self) -> bool { + matches!(self.inclusion, RangeInclusion::Inclusive) + } + + pub fn contains(&self, item: &Value) -> bool { + match (item.partial_cmp(&self.from), item.partial_cmp(&self.to)) { + (Some(Ordering::Greater | Ordering::Equal), Some(Ordering::Less)) => self.moves_up(), + (Some(Ordering::Less | Ordering::Equal), Some(Ordering::Greater)) => !self.moves_up(), + (Some(_), Some(Ordering::Equal)) => self.is_end_inclusive(), + (_, _) => false, + } + } } impl IntoIterator for Range { @@ -129,6 +148,9 @@ pub struct RangeIterator { impl RangeIterator { pub fn new(range: Range, span: Span) -> RangeIterator { + let moves_up = range.moves_up(); + let is_end_inclusive = range.is_end_inclusive(); + let start = match range.from { Value::Nothing { .. } => Value::Int { val: 0, span }, x => x, @@ -143,29 +165,17 @@ impl RangeIterator { }; RangeIterator { - moves_up: matches!(start.lte(span, &end), Ok(Value::Bool { val: true, .. })), + moves_up, curr: start, end, span, - is_end_inclusive: matches!(range.inclusion, RangeInclusion::Inclusive), + is_end_inclusive, done: false, incr: range.incr, } } } -// Compare two floating point numbers. The decision interval for equality is dynamically scaled -// as the value being compared increases in magnitude. -fn compare_floats(val: f64, other: f64) -> Option { - let prec = f64::EPSILON.max(val.abs() * f64::EPSILON); - - if (other - val).abs() < prec { - return Some(Ordering::Equal); - } - - val.partial_cmp(&other) -} - impl Iterator for RangeIterator { type Item = Value; fn next(&mut self) -> Option { @@ -176,19 +186,7 @@ impl Iterator for RangeIterator { let ordering = if matches!(self.end, Value::Nothing { .. }) { Some(Ordering::Less) } else { - match (&self.curr, &self.end) { - (Value::Int { val: curr, .. }, Value::Int { val: end, .. }) => Some(curr.cmp(end)), - (Value::Float { val: curr, .. }, Value::Float { val: end, .. }) => { - compare_floats(*curr, *end) - } - (Value::Float { val: curr, .. }, Value::Int { val: end, .. }) => { - compare_floats(*curr, *end as f64) - } - (Value::Int { val: curr, .. }, Value::Float { val: end, .. }) => { - compare_floats(*curr as f64, *end) - } - _ => None, - } + self.curr.partial_cmp(&self.end) }; let ordering = if let Some(ord) = ordering { diff --git a/src/tests.rs b/src/tests.rs index 4c2c96191d..eb431e8a47 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -569,3 +569,76 @@ fn split_column() -> TestResult { fn for_loops() -> TestResult { run_test(r#"(for x in [1, 2, 3] { $x + 10 }).1"#, "12") } + +#[test] +fn type_in_list_of_this_type() -> TestResult { + run_test(r#"42 in [41 42 43]"#, "true") +} + +#[test] +fn type_in_list_of_non_this_type() -> TestResult { + fail_test(r#"'hello' in [41 42 43]"#, "mismatched for operation") +} + +#[test] +fn string_in_string() -> TestResult { + run_test(r#"'z' in 'abc'"#, "false") +} + +#[test] +fn non_string_in_string() -> TestResult { + fail_test(r#"42 in 'abc'"#, "mismatched for operation") +} + +#[test] +fn int_in_inc_range() -> TestResult { + run_test(r#"1 in -4..9.42"#, "true") +} + +#[test] +fn int_in_dec_range() -> TestResult { + run_test(r#"1 in 9.42..-4"#, "true") +} + +#[test] +fn int_in_exclusive_range() -> TestResult { + run_test(r#"3 in 0..<3"#, "false") +} + +#[test] +fn non_number_in_range() -> TestResult { + fail_test(r#"'a' in 1..3"#, "mismatched for operation") +} + +#[test] +fn string_in_record() -> TestResult { + run_test(r#""a" in ('{ "a": 13, "b": 14 }' | from json)"#, "true") +} + +#[test] +fn non_string_in_record() -> TestResult { + fail_test( + r#"4 in ('{ "a": 13, "b": 14 }' | from json)"#, + "mismatch during operation", + ) +} + +#[test] +fn string_in_valuestream() -> TestResult { + run_test( + r#" + 'Hello' in ("Hello + World" | lines)"#, + "true", + ) +} + +#[test] +fn string_not_in_string() -> TestResult { + run_test(r#"'d' not-in 'abc'"#, "true") +} + +#[test] +fn float_not_in_inc_range() -> TestResult { + run_test(r#"1.4 not-in 2..9.42"#, "true") +}