diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index 9dfbdf7..cf91c7c 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -162,8 +162,10 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { .add(crate::lang::vocabulary::filters::field_filter_maybe()) .add(crate::lang::vocabulary::filters::index_filter()) .add(crate::lang::vocabulary::filters::range_filter()) + .add(crate::lang::vocabulary::filters::field_like_filter()) // sorters .add(crate::lang::vocabulary::sorters::empty_sort()) + .add(crate::lang::vocabulary::sorters::field_sort()) // functions and misc .add(crate::lang::vocabulary::sql_function_factory()) .add(crate::lang::vocabulary::simple_sql_function_factory()) diff --git a/mps-interpreter/src/lang/type_primitives.rs b/mps-interpreter/src/lang/type_primitives.rs index 268a41d..3469c16 100644 --- a/mps-interpreter/src/lang/type_primitives.rs +++ b/mps-interpreter/src/lang/type_primitives.rs @@ -1,8 +1,9 @@ //! Basic types for MPS use std::fmt::{Debug, Display, Error, Formatter}; +use std::cmp::{Ordering, Ord}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum MpsTypePrimitive { String(String), Int(i64), @@ -14,60 +15,9 @@ pub enum MpsTypePrimitive { impl MpsTypePrimitive { #[inline] pub fn compare(&self, other: &Self) -> Result { - let result = match self { - Self::String(s1) => match other { - Self::String(s2) => Some(map_ordering(s1.cmp(s2))), - _ => None, - }, - Self::Int(i1) => match other { - Self::Int(i2) => Some(map_ordering(i1.cmp(i2))), - Self::UInt(i2) => Some(map_ordering((*i1 as i128).cmp(&(*i2 as i128)))), - Self::Float(i2) => Some(map_ordering( - (*i1 as f64) - .partial_cmp(&(*i2 as f64)) - .unwrap_or(std::cmp::Ordering::Less), - )), - _ => None, - }, - Self::UInt(u1) => match other { - Self::UInt(u2) => Some(map_ordering(u1.cmp(u2))), - Self::Int(u2) => Some(map_ordering((*u1 as i128).cmp(&(*u2 as i128)))), - Self::Float(u2) => Some(map_ordering( - (*u1 as f64) - .partial_cmp(&(*u2 as f64)) - .unwrap_or(std::cmp::Ordering::Less), - )), - _ => None, - }, - Self::Float(f1) => match other { - Self::Float(f2) => Some(map_ordering( - f1.partial_cmp(f2).unwrap_or(std::cmp::Ordering::Less), - )), - Self::Int(f2) => Some(map_ordering( - f1.partial_cmp(&(*f2 as f64)) - .unwrap_or(std::cmp::Ordering::Less), - )), - Self::UInt(f2) => Some(map_ordering( - f1.partial_cmp(&(*f2 as f64)) - .unwrap_or(std::cmp::Ordering::Less), - )), - _ => None, - }, - Self::Bool(b1) => match other { - Self::Bool(b2) => { - if *b2 == *b1 { - Some(0) - } else if *b1 { - Some(1) - } else { - Some(-1) - } - } - _ => None, - }, - }; + let result = self.partial_cmp(other); match result { - Some(x) => Ok(x), + Some(x) => Ok(map_ordering(x)), None => Err(format!( "Cannot compare {} to {}: incompatible types", self, other @@ -82,6 +32,16 @@ impl MpsTypePrimitive { } } + pub fn as_str(&self) -> String { + match self { + Self::String(s) => s.clone(), + Self::UInt(x) => format!("{}", x), + Self::Int(x) => format!("{}", x), + Self::Float(x) => format!("{}", x), + Self::Bool(x) => format!("{}", x) + } + } + pub fn to_u64(self) -> Option { match self { Self::UInt(x) => Some(x), @@ -120,11 +80,68 @@ impl MpsTypePrimitive { impl Display for MpsTypePrimitive { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - Self::String(s) => write!(f, "(String) {}", s), - Self::Int(i) => write!(f, "(Int) {}", *i), - Self::UInt(u) => write!(f, "(UInt) {}", *u), - Self::Float(f_) => write!(f, "(Float) {}", *f_), - Self::Bool(b) => write!(f, "(Bool) {}", *b), + Self::String(s) => write!(f, "String[`{}`]", s), + Self::Int(i) => write!(f, "Int[{}]", *i), + Self::UInt(u) => write!(f, "UInt[{}]", *u), + Self::Float(f_) => write!(f, "Float[{}]", *f_), + Self::Bool(b) => write!(f, "Bool[{}]", *b), + } + } +} + +impl PartialOrd for MpsTypePrimitive { + fn partial_cmp(&self, other: &Self) -> Option { + match self { + Self::String(s1) => match other { + Self::String(s2) => Some(s1.cmp(s2)), + _ => None, + }, + Self::Int(i1) => match other { + Self::Int(i2) => Some(i1.cmp(i2)), + Self::UInt(i2) => Some((*i1 as i128).cmp(&(*i2 as i128))), + Self::Float(i2) => Some( + (*i1 as f64) + .partial_cmp(&(*i2 as f64)) + .unwrap_or(std::cmp::Ordering::Less), + ), + _ => None, + }, + Self::UInt(u1) => match other { + Self::UInt(u2) => Some(u1.cmp(u2)), + Self::Int(u2) => Some((*u1 as i128).cmp(&(*u2 as i128))), + Self::Float(u2) => Some( + (*u1 as f64) + .partial_cmp(&(*u2 as f64)) + .unwrap_or(std::cmp::Ordering::Less), + ), + _ => None, + }, + Self::Float(f1) => match other { + Self::Float(f2) => Some( + f1.partial_cmp(f2).unwrap_or(std::cmp::Ordering::Less), + ), + Self::Int(f2) => Some( + f1.partial_cmp(&(*f2 as f64)) + .unwrap_or(std::cmp::Ordering::Less), + ), + Self::UInt(f2) => Some( + f1.partial_cmp(&(*f2 as f64)) + .unwrap_or(std::cmp::Ordering::Less), + ), + _ => None, + }, + Self::Bool(b1) => match other { + Self::Bool(b2) => { + if *b2 == *b1 { + Some(std::cmp::Ordering::Equal) + } else if *b1 { + Some(std::cmp::Ordering::Greater) + } else { + Some(std::cmp::Ordering::Less) + } + } + _ => None, + }, } } } diff --git a/mps-interpreter/src/lang/vocabulary/filters/field_like_filter.rs b/mps-interpreter/src/lang/vocabulary/filters/field_like_filter.rs new file mode 100644 index 0000000..043f63d --- /dev/null +++ b/mps-interpreter/src/lang/vocabulary/filters/field_like_filter.rs @@ -0,0 +1,158 @@ +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Error, Formatter}; + +use super::field_filter::{VariableOrValue, FieldFilterErrorHandling}; +use crate::lang::utility::{assert_token, check_name, assert_name, assert_token_raw}; +use crate::lang::MpsLanguageDictionary; +use crate::lang::MpsTypePrimitive; +use crate::lang::{MpsFilterFactory, MpsFilterPredicate, MpsFilterStatementFactory}; +use crate::lang::{RuntimeError, SyntaxError}; +use crate::processing::general::MpsType; +use crate::processing::OpGetter; +use crate::tokens::MpsToken; +use crate::MpsContext; +use crate::MpsItem; + +#[derive(Debug, Clone)] +pub struct FieldLikeFilter { + field_name: String, + field_errors: FieldFilterErrorHandling, + val: VariableOrValue, +} + +impl Display for FieldLikeFilter { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match &self.val { + VariableOrValue::Variable(name) => write!(f, "{} like {}", self.field_name, name), + VariableOrValue::Value(t) => write!(f, "{} like {}", self.field_name, t), + } + } +} + +impl MpsFilterPredicate for FieldLikeFilter { + fn matches( + &mut self, + music_item_lut: &MpsItem, + ctx: &mut MpsContext, + op: &mut OpGetter, + ) -> Result { + let variable = match &self.val { + VariableOrValue::Variable(name) => match ctx.variables.get(&name, op)? { + MpsType::Primitive(MpsTypePrimitive::String(s)) => Ok(s), + _ => Err(RuntimeError { + line: 0, + op: op(), + msg: format!("Variable {} is not comparable", name), + }), + }, + VariableOrValue::Value(MpsTypePrimitive::String(s)) => Ok(s), + // non-string values will be stopped at parse-time, so this should never occur + _ => Err(RuntimeError { + line: 0, + op: op(), + msg: format!("Value is not type String"), + }) + }?; + if let Some(field) = music_item_lut.field(&self.field_name) { + let field_str = field.as_str().to_lowercase(); + Ok(field_str.contains(&variable.to_lowercase())) + } else { + match self.field_errors { + FieldFilterErrorHandling::Error => Err(RuntimeError { + line: 0, + op: op(), + msg: format!("Field {} does not exist", &self.field_name), + }), + FieldFilterErrorHandling::Ignore => Ok(false), + FieldFilterErrorHandling::Include => Ok(true), + } + } + } + + fn is_complete(&self) -> bool { + false + } + + fn reset(&mut self) -> Result<(), RuntimeError> { + Ok(()) + } +} + +pub struct FieldLikeFilterFactory; + +impl MpsFilterFactory for FieldLikeFilterFactory { + fn is_filter(&self, tokens: &VecDeque<&MpsToken>) -> bool { + let tokens_len = tokens.len(); + (tokens_len == 3 // field like variable + && tokens[0].is_name() + && check_name("like", &tokens[1]) + && (tokens[2].is_name() || tokens[2].is_literal())) + || (tokens_len == 4 // field? like variable OR field! like variable + && tokens[0].is_name() + && (tokens[1].is_interrogation() || tokens[1].is_exclamation()) + && check_name("like", &tokens[2]) + && (tokens[3].is_name() || tokens[3].is_literal())) + } + + fn build_filter( + &self, + tokens: &mut VecDeque, + _dict: &MpsLanguageDictionary, + ) -> Result { + let field = assert_token( + |t| match t { + MpsToken::Name(n) => Some(n), + _ => None, + }, + MpsToken::Name("field_name".into()), + tokens, + )?; + let error_handling = if tokens[0].is_interrogation() { + assert_token_raw(MpsToken::Interrogation, tokens)?; + FieldFilterErrorHandling::Ignore + } else if tokens[0].is_exclamation() { + assert_token_raw(MpsToken::Exclamation, tokens)?; + FieldFilterErrorHandling::Include + } else { + FieldFilterErrorHandling::Error + }; + assert_name("like", tokens)?; + if tokens[0].is_literal() { + let literal = assert_token( + |t| match t { + MpsToken::Literal(n) => Some(n), + _ => None, + }, + MpsToken::Literal("like_string".into()), + tokens, + )?; + let value = VariableOrValue::Value(MpsTypePrimitive::String(literal)); + Ok(FieldLikeFilter { + field_name: field, + field_errors: error_handling, + val: value, + }) + } else { + let variable = VariableOrValue::Variable(assert_token( + |t| match t { + MpsToken::Name(n) => Some(n), + _ => None, + }, + MpsToken::Name("variable_name".into()), + tokens, + )?); + Ok(FieldLikeFilter { + field_name: field, + field_errors: FieldFilterErrorHandling::Error, + val: variable, + }) + } + } +} + +pub type FieldLikeFilterStatementFactory = MpsFilterStatementFactory; + +#[inline(always)] +pub fn field_like_filter() -> FieldLikeFilterStatementFactory { + FieldLikeFilterStatementFactory::new(FieldLikeFilterFactory) +} diff --git a/mps-interpreter/src/lang/vocabulary/filters/mod.rs b/mps-interpreter/src/lang/vocabulary/filters/mod.rs index 101fbc5..979267e 100644 --- a/mps-interpreter/src/lang/vocabulary/filters/mod.rs +++ b/mps-interpreter/src/lang/vocabulary/filters/mod.rs @@ -1,6 +1,7 @@ mod empty_filter; mod field_filter; mod field_filter_maybe; +mod field_like_filter; mod index_filter; mod range_filter; pub(crate) mod utility; @@ -12,5 +13,6 @@ pub use field_filter::{ field_filter, FieldFilter, FieldFilterFactory, FieldFilterStatementFactory, FieldFilterErrorHandling, }; pub use field_filter_maybe::{field_filter_maybe, FieldFilterMaybeFactory, FieldFilterMaybeStatementFactory}; +pub use field_like_filter::{field_like_filter, FieldLikeFilterFactory, FieldLikeFilterStatementFactory}; pub use index_filter::{index_filter, IndexFilter, IndexFilterFactory, IndexFilterStatementFactory}; pub use range_filter::{range_filter, RangeFilter, RangeFilterFactory, RangeFilterStatementFactory}; diff --git a/mps-interpreter/src/lang/vocabulary/sorters/field_sorter.rs b/mps-interpreter/src/lang/vocabulary/sorters/field_sorter.rs new file mode 100644 index 0000000..77b034a --- /dev/null +++ b/mps-interpreter/src/lang/vocabulary/sorters/field_sorter.rs @@ -0,0 +1,87 @@ +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::cmp::Ordering; + +use crate::lang::{MpsSorter, MpsSorterFactory, MpsSortStatementFactory}; +use crate::lang::{MpsLanguageDictionary, MpsIteratorItem, MpsOp}; +use crate::lang::{RuntimeError, SyntaxError}; +use crate::lang::utility::assert_token; +use crate::tokens::MpsToken; + +#[derive(Debug, Clone)] +pub struct FieldSorter { + field_name: String, + up_to: usize, + default_order: Ordering, +} + +impl MpsSorter for FieldSorter { + fn sort(&mut self, iterator: &mut dyn MpsOp, item_buf: &mut VecDeque) -> Result<(), RuntimeError> { + let buf_len_old = item_buf.len(); // save buffer length before modifying buffer + if item_buf.len() < self.up_to { + for item in iterator { + item_buf.push_back(item); + if item_buf.len() >= self.up_to { + break; + } + } + } + if buf_len_old != item_buf.len() { + // when buf_len_old == item_buf.len(), iterator was already complete + // no need to sort in that case, since buffer was sorted in last call to sort or buffer never had any items to sort + item_buf.make_contiguous().sort_by( + |a, b| { + if let Ok(a) = a { + if let Some(a_field) = a.field(&self.field_name) { + if let Ok(b) = b { + if let Some(b_field) = b.field(&self.field_name) { + return a_field.partial_cmp(b_field).unwrap_or(self.default_order); + } + } + } + } + self.default_order + } + ); + println!("Field-sorted item_buf: {:?}", item_buf); + } + Ok(()) + } +} + +impl Display for FieldSorter { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}", self.field_name) + } +} + +pub struct FieldSorterFactory; + +impl MpsSorterFactory for FieldSorterFactory { + fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool { + tokens.len() == 1 && tokens[0].is_name() + } + + fn build_sorter( + &self, + tokens: &mut VecDeque, + _dict: &MpsLanguageDictionary, + ) -> Result { + let name = assert_token(|t| match t { + MpsToken::Name(s) => Some(s), + _ => None + }, MpsToken::Name("field_name".into()), tokens)?; + Ok(FieldSorter { + field_name: name, + up_to: usize::MAX, + default_order: Ordering::Equal + }) + } +} + +pub type FieldSorterStatementFactory = MpsSortStatementFactory; + +#[inline(always)] +pub fn field_sort() -> FieldSorterStatementFactory { + FieldSorterStatementFactory::new(FieldSorterFactory) +} diff --git a/mps-interpreter/src/lang/vocabulary/sorters/mod.rs b/mps-interpreter/src/lang/vocabulary/sorters/mod.rs index 26a771a..57c0443 100644 --- a/mps-interpreter/src/lang/vocabulary/sorters/mod.rs +++ b/mps-interpreter/src/lang/vocabulary/sorters/mod.rs @@ -1,3 +1,5 @@ mod empty_sorter; +mod field_sorter; pub use empty_sorter::{empty_sort, EmptySorter, EmptySorterFactory, EmptySorterStatementFactory}; +pub use field_sorter::{field_sort, FieldSorter, FieldSorterFactory, FieldSorterStatementFactory}; diff --git a/mps-interpreter/src/processing/filesystem.rs b/mps-interpreter/src/processing/filesystem.rs index 25e57fe..edf988a 100644 --- a/mps-interpreter/src/processing/filesystem.rs +++ b/mps-interpreter/src/processing/filesystem.rs @@ -289,6 +289,7 @@ impl Iterator for FileIter { } } } + self.is_complete = true; None } } diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs index 1040622..4d01b3c 100644 --- a/mps-interpreter/tests/single_line.rs +++ b/mps-interpreter/tests/single_line.rs @@ -300,3 +300,36 @@ fn execute_emptysort_line() -> Result<(), Box> { true, ) } + +#[test] +fn execute_likefilter_line() -> Result<(), Box> { + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(not_a_field? like `24K Magic`)", + true, + true, + )?; + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(not_a_field! like `24K Magic`)", + false, + true, + )?; + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(album like `24K Magic`)", + false, + true, + ) +} + +#[test] +fn execute_fieldsort_line() -> Result<(), Box> { + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(title)", + false, + true, + )?; + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).sort(not_a_field)", + false, + true, + ) +}