Add sort by field name

This commit is contained in:
NGnius (Graham) 2022-01-24 16:12:00 -05:00
parent 9ac5dd4570
commit 8dc571521f
8 changed files with 361 additions and 59 deletions

View file

@ -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())

View file

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

View 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)
}

View file

@ -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};

View 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)
}

View file

@ -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};

View file

@ -289,6 +289,7 @@ impl Iterator for FileIter {
}
}
}
self.is_complete = true;
None
}
}

View file

@ -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,
)
}