Add sort by field name
This commit is contained in:
parent
9ac5dd4570
commit
8dc571521f
8 changed files with 361 additions and 59 deletions
|
@ -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())
|
||||
|
|
|
@ -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<i8, String> {
|
||||
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<u64> {
|
||||
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<Ordering> {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
158
mps-interpreter/src/lang/vocabulary/filters/field_like_filter.rs
Normal file
158
mps-interpreter/src/lang/vocabulary/filters/field_like_filter.rs
Normal file
|
@ -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<bool, RuntimeError> {
|
||||
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<FieldLikeFilter> 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<MpsToken>,
|
||||
_dict: &MpsLanguageDictionary,
|
||||
) -> Result<FieldLikeFilter, SyntaxError> {
|
||||
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<FieldLikeFilter, FieldLikeFilterFactory>;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn field_like_filter() -> FieldLikeFilterStatementFactory {
|
||||
FieldLikeFilterStatementFactory::new(FieldLikeFilterFactory)
|
||||
}
|
|
@ -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};
|
||||
|
|
87
mps-interpreter/src/lang/vocabulary/sorters/field_sorter.rs
Normal file
87
mps-interpreter/src/lang/vocabulary/sorters/field_sorter.rs
Normal file
|
@ -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<MpsIteratorItem>) -> 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<FieldSorter> for FieldSorterFactory {
|
||||
fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool {
|
||||
tokens.len() == 1 && tokens[0].is_name()
|
||||
}
|
||||
|
||||
fn build_sorter(
|
||||
&self,
|
||||
tokens: &mut VecDeque<MpsToken>,
|
||||
_dict: &MpsLanguageDictionary,
|
||||
) -> Result<FieldSorter, SyntaxError> {
|
||||
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<FieldSorter, FieldSorterFactory>;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn field_sort() -> FieldSorterStatementFactory {
|
||||
FieldSorterStatementFactory::new(FieldSorterFactory)
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -289,6 +289,7 @@ impl Iterator for FileIter {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.is_complete = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,3 +300,36 @@ fn execute_emptysort_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
|||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_likefilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
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<dyn MpsLanguageError>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue