Add all replacement filter functionality
This commit is contained in:
parent
dd5f15c745
commit
b9def2f15c
12 changed files with 588 additions and 90 deletions
|
@ -51,26 +51,37 @@ field != something
|
|||
field >= something
|
||||
field > something
|
||||
field <= something
|
||||
field < something -- e.g. iterable.(title == "Romantic Traffic");
|
||||
field < something -- e.g. `iterable.(title == "Romantic Traffic");`
|
||||
|
||||
Compare all items, keeping only those that match the condition. Valid field names are those of the MpsMusicItem (title, artist, album, genre, track, etc.), though this will change when proper object support is added. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively).
|
||||
|
||||
start..end -- e.g. iterable.(0..42);
|
||||
start..end -- e.g. `iterable.(0..42);`
|
||||
|
||||
Keep only the items that are at the start index up to the end index. Start and/or end may be omitted to start/stop at the iterable's existing start/end (respectively). This stops once the end condition is met, leaving the rest of the iterator unconsumed.
|
||||
|
||||
start..=end -- e.g. iterable.(0..=42);
|
||||
start..=end -- e.g. `iterable.(0..=42);`
|
||||
|
||||
Keep only the items that are at the start index up to and including the end index. Start may be omitted to start at the iterable's existing start. This stops once the end condition is met, leaving the rest of the iterator unconsumed.
|
||||
|
||||
index -- e.g. iterable.(4);
|
||||
index -- e.g. `iterable.(4);`
|
||||
|
||||
Keep only the item at the given index. This stops once the index is reached, leaving the rest of the iterator unconsumed.
|
||||
|
||||
predicate1 || predicate2 -- e.g. iterable.(4 || 5);
|
||||
predicate1 || predicate2 -- e.g. `iterable.(4 || 5);`
|
||||
|
||||
Keep only the items that meet the criteria of predicate1 or predicate2. This will always consume the full iterator.
|
||||
|
||||
[empty] -- e.g. iterable.();
|
||||
[empty] -- e.g. `iterable.();`
|
||||
|
||||
Matches all items
|
||||
|
||||
if filter: operation1 else operation2 -- e.g. `iterable.(if title == "Romantic Traffic": repeat(item, 2) else item.());`
|
||||
|
||||
Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`.
|
||||
|
||||
### Functions
|
||||
Similar to most other languages: `function_name(param1, param2, etc.);`.
|
||||
These always return an iterable which can me manipulated.
|
||||
Functions are statements of the format `function_name(params)`, where "function_name" is one of the function names (below) and params is a valid parameter input for the function.
|
||||
Each function is responsible for parsing it's own parameters when the statement is parsed, so this is very flexible.
|
||||
E.g. `files(folder="~/Music/", recursive=true);` is valid function syntax to execute the files function with parameters `folder="~/Music/", recursive=true`.
|
||||
|
|
|
@ -3,11 +3,12 @@ use std::fmt::{Debug, Display, Error, Formatter};
|
|||
use std::iter::Iterator;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::lang::utility::{assert_token, assert_token_raw};
|
||||
use crate::lang::utility::{assert_token, assert_token_raw, check_name, assert_name};
|
||||
use crate::lang::MpsLanguageDictionary;
|
||||
use crate::lang::{BoxedMpsOpFactory, MpsOp, PseudoOp};
|
||||
use crate::lang::{RuntimeError, SyntaxError};
|
||||
use crate::lang::SingleItem;
|
||||
use crate::lang::MpsFilterReplaceStatement;
|
||||
use crate::processing::general::MpsType;
|
||||
use crate::processing::OpGetter;
|
||||
use crate::tokens::MpsToken;
|
||||
|
@ -40,7 +41,7 @@ pub trait MpsFilterFactory<P: MpsFilterPredicate + 'static> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum VariableOrOp {
|
||||
pub(super) enum VariableOrOp {
|
||||
Variable(String),
|
||||
Op(PseudoOp),
|
||||
}
|
||||
|
@ -75,7 +76,11 @@ impl<P: MpsFilterPredicate + 'static> std::clone::Clone for MpsFilterStatement<P
|
|||
|
||||
impl<P: MpsFilterPredicate + 'static> Display for MpsFilterStatement<P> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
write!(f, "{}.({})", self.iterable, self.predicate)
|
||||
if let Some(other_filters) = &self.other_filters {
|
||||
write!(f, "{}.({} || (like) {})", self.iterable, self.predicate, other_filters)
|
||||
} else {
|
||||
write!(f, "{}.({})", self.iterable, self.predicate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,18 +94,25 @@ impl<P: MpsFilterPredicate + 'static> MpsOp for MpsFilterStatement<P> {
|
|||
}
|
||||
|
||||
fn is_resetable(&self) -> bool {
|
||||
match &self.iterable {
|
||||
let is_iterable_resetable = match &self.iterable {
|
||||
VariableOrOp::Variable(s) => {
|
||||
let var = self.context.as_ref().unwrap().variables.get_opt(s);
|
||||
if let Some(MpsType::Op(var)) = var {
|
||||
var.is_resetable()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if self.context.is_some() {
|
||||
let var = self.context.as_ref().unwrap().variables.get_opt(s);
|
||||
if let Some(MpsType::Op(var)) = var {
|
||||
var.is_resetable()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {true} // ASSUMPTION
|
||||
|
||||
}
|
||||
VariableOrOp::Op(PseudoOp::Real(op)) => op.is_resetable(),
|
||||
VariableOrOp::Op(PseudoOp::Fake(_)) => false,
|
||||
}
|
||||
};
|
||||
let is_other_filter_resetable = if let Some(PseudoOp::Real(other_filter)) = &self.other_filters {
|
||||
other_filter.is_resetable()
|
||||
} else {true};
|
||||
is_iterable_resetable && is_other_filter_resetable
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), RuntimeError> {
|
||||
|
@ -108,30 +120,43 @@ impl<P: MpsFilterPredicate + 'static> MpsOp for MpsFilterStatement<P> {
|
|||
self.predicate.reset()?;
|
||||
match &mut self.iterable {
|
||||
VariableOrOp::Variable(s) => {
|
||||
let fake_getter = &mut move || fake.clone();
|
||||
if let MpsType::Op(var) = self
|
||||
.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.variables
|
||||
.get_mut(s, fake_getter)?
|
||||
{
|
||||
var.reset()
|
||||
} else {
|
||||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: PseudoOp::Fake(format!("{}", self)),
|
||||
msg: "Cannot reset non-iterable filter variable".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
VariableOrOp::Op(PseudoOp::Real(op)) => op.reset(),
|
||||
if self.context.as_mut().unwrap().variables.exists(s) {
|
||||
let fake_getter = &mut move || fake.clone();
|
||||
let mut var = self.context.as_mut().unwrap().variables.remove(s, fake_getter)?;
|
||||
let result = if let MpsType::Op(var) = &mut var {
|
||||
var.enter(self.context.take().unwrap());
|
||||
let result = var.reset();
|
||||
self.context = Some(var.escape());
|
||||
result
|
||||
} else {
|
||||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: fake_getter(),
|
||||
msg: "Cannot reset non-iterable filter variable".to_string(),
|
||||
})
|
||||
};
|
||||
self.context.as_mut().unwrap().variables.declare(s, var, fake_getter)?;
|
||||
result
|
||||
} else {Ok(())}
|
||||
},
|
||||
VariableOrOp::Op(PseudoOp::Real(op)) => {
|
||||
op.enter(self.context.take().unwrap());
|
||||
let result = op.reset();
|
||||
self.context = Some(op.escape());
|
||||
result
|
||||
},
|
||||
VariableOrOp::Op(PseudoOp::Fake(_)) => Err(RuntimeError {
|
||||
line: 0,
|
||||
op: fake,
|
||||
msg: "Cannot reset PseudoOp::Fake filter".to_string(),
|
||||
}),
|
||||
}
|
||||
}?;
|
||||
if let Some(PseudoOp::Real(other_filter)) = &mut self.other_filters {
|
||||
other_filter.enter(self.context.take().unwrap());
|
||||
let result = other_filter.reset();
|
||||
self.context = Some(other_filter.escape());
|
||||
result
|
||||
} else {Ok(())}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,14 +363,30 @@ impl<P: MpsFilterPredicate + 'static, F: MpsFilterFactory<P> + 'static> BoxedMps
|
|||
if start_of_predicate > tokens_len - 1 {
|
||||
false
|
||||
} else {
|
||||
if let Some(pipe_location) = first_double_pipe(tokens) {
|
||||
let pipe_location_opt = last_double_pipe(tokens, 1);
|
||||
if pipe_location_opt.is_some() && pipe_location_opt.unwrap() > start_of_predicate {
|
||||
let pipe_location = pipe_location_opt.unwrap();
|
||||
// filters combined by OR operations
|
||||
let tokens2: VecDeque<&MpsToken> =
|
||||
VecDeque::from_iter(tokens.range(start_of_predicate..pipe_location));
|
||||
self.filter_factory.is_filter(&tokens2)
|
||||
} else {
|
||||
// single filter
|
||||
let tokens2: VecDeque<&MpsToken> =
|
||||
VecDeque::from_iter(tokens.range(start_of_predicate..tokens_len - 1));
|
||||
self.filter_factory.is_filter(&tokens2)
|
||||
if tokens2.len() != 0 && check_name("if", &tokens2[0]) {
|
||||
// replacement filter
|
||||
if let Some(colon_location) = first_colon2(&tokens2) {
|
||||
let tokens3 = VecDeque::from_iter(tokens.range(start_of_predicate+1..start_of_predicate+colon_location));
|
||||
self.filter_factory.is_filter(&tokens3)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// regular filter
|
||||
self.filter_factory.is_filter(&tokens2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -382,40 +423,95 @@ impl<P: MpsFilterPredicate + 'static, F: MpsFilterFactory<P> + 'static> BoxedMps
|
|||
}
|
||||
assert_token_raw(MpsToken::Dot, tokens)?;
|
||||
assert_token_raw(MpsToken::OpenBracket, tokens)?;
|
||||
let mut another_filter = None;
|
||||
let (has_or, end_tokens) = if let Some(pipe_location) = first_double_pipe(tokens) {
|
||||
(true, tokens.split_off(pipe_location)) // parse up to OR operator
|
||||
if !tokens.is_empty() && check_name("if", &tokens[0]) {
|
||||
return {
|
||||
// replacement filter
|
||||
//println!("Building replacement filter from tokens {:?}", tokens);
|
||||
assert_name("if", tokens)?;
|
||||
if let Some(colon_location) = first_colon(tokens) {
|
||||
let end_tokens = tokens.split_off(colon_location);
|
||||
let filter = self.filter_factory.build_filter(tokens, dict)?;
|
||||
tokens.extend(end_tokens);
|
||||
assert_token_raw(MpsToken::Colon, tokens)?;
|
||||
let mut else_op: Option<PseudoOp> = None;
|
||||
let if_op: PseudoOp;
|
||||
if let Some(else_location) = first_else_not_in_bracket(tokens) {
|
||||
let end_tokens = tokens.split_off(else_location);
|
||||
// build replacement system
|
||||
if_op = dict.try_build_statement(tokens)?.into();
|
||||
tokens.extend(end_tokens);
|
||||
assert_name("else", tokens)?;
|
||||
let end_tokens = tokens.split_off(tokens.len() - 1); // up to ending close bracket
|
||||
// build replacement system
|
||||
else_op = Some(dict.try_build_statement(tokens)?.into());
|
||||
tokens.extend(end_tokens);
|
||||
} else {
|
||||
let end_tokens = tokens.split_off(tokens.len() - 1);
|
||||
// build replacement system
|
||||
if_op = dict.try_build_statement(tokens)?.into();
|
||||
tokens.extend(end_tokens);
|
||||
}
|
||||
assert_token_raw(MpsToken::CloseBracket, tokens)?;
|
||||
Ok(Box::new(MpsFilterReplaceStatement {
|
||||
predicate: filter,
|
||||
iterable: op,
|
||||
context: None,
|
||||
op_if: if_op,
|
||||
op_else: else_op,
|
||||
item_cache: super::filter_replace::item_cache_deque()
|
||||
}))
|
||||
} else {
|
||||
Err(SyntaxError {
|
||||
line: 0,
|
||||
token: MpsToken::Colon,
|
||||
got: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(false, tokens.split_off(tokens.len()-1)) // don't parse closing bracket in filter
|
||||
};
|
||||
let filter = self.filter_factory.build_filter(tokens, dict)?;
|
||||
tokens.extend(end_tokens);
|
||||
if has_or {
|
||||
// recursively build other filters for OR operation
|
||||
assert_token_raw(MpsToken::Pipe, tokens)?;
|
||||
assert_token_raw(MpsToken::Pipe, tokens)?;
|
||||
// emit fake filter syntax
|
||||
tokens.push_front(MpsToken::OpenBracket);
|
||||
tokens.push_front(MpsToken::Dot);
|
||||
tokens.push_front(MpsToken::Name(INNER_VARIABLE_NAME.into())); // impossible to obtain through parsing on purpose
|
||||
another_filter = Some(dict.try_build_statement(tokens)?.into());
|
||||
} else {
|
||||
assert_token_raw(MpsToken::CloseBracket, tokens)?; // remove closing bracket
|
||||
let mut another_filter = None;
|
||||
let (has_or, end_tokens) = if let Some(pipe_location) = last_double_pipe(tokens, 1) {
|
||||
(true, tokens.split_off(pipe_location)) // parse up to OR operator
|
||||
} else {
|
||||
(false, tokens.split_off(tokens.len()-1)) // don't parse closing bracket in filter
|
||||
};
|
||||
let filter = self.filter_factory.build_filter(tokens, dict)?;
|
||||
tokens.extend(end_tokens);
|
||||
if has_or {
|
||||
// recursively build other filters for OR operation
|
||||
assert_token_raw(MpsToken::Pipe, tokens)?;
|
||||
assert_token_raw(MpsToken::Pipe, tokens)?;
|
||||
// emit fake filter syntax
|
||||
tokens.push_front(MpsToken::OpenBracket);
|
||||
tokens.push_front(MpsToken::Dot);
|
||||
tokens.push_front(MpsToken::Name(INNER_VARIABLE_NAME.into())); // impossible to obtain through parsing on purpose
|
||||
another_filter = Some(dict.try_build_statement(tokens)?.into());
|
||||
} else {
|
||||
assert_token_raw(MpsToken::CloseBracket, tokens)?; // remove closing bracket
|
||||
}
|
||||
Ok(Box::new(MpsFilterStatement {
|
||||
predicate: filter,
|
||||
iterable: op,
|
||||
context: None,
|
||||
other_filters: another_filter,
|
||||
}))
|
||||
}
|
||||
Ok(Box::new(MpsFilterStatement {
|
||||
predicate: filter,
|
||||
iterable: op,
|
||||
context: None,
|
||||
other_filters: another_filter,
|
||||
}))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn last_open_bracket_is_after_dot(tokens: &VecDeque<MpsToken>) -> bool {
|
||||
let mut inside_brackets = 0;
|
||||
let mut open_bracket_found = false;
|
||||
for i in (0..tokens.len()).rev() {
|
||||
if tokens[i].is_open_bracket() {
|
||||
open_bracket_found = true;
|
||||
if tokens[i].is_close_bracket() {
|
||||
inside_brackets += 1;
|
||||
} else if tokens[i].is_open_bracket() {
|
||||
if inside_brackets == 1 {
|
||||
open_bracket_found = true;
|
||||
} else if inside_brackets != 0 {
|
||||
inside_brackets -= 1;
|
||||
}
|
||||
} else if open_bracket_found {
|
||||
if tokens[i].is_dot() {
|
||||
return true;
|
||||
|
@ -428,10 +524,17 @@ fn last_open_bracket_is_after_dot(tokens: &VecDeque<MpsToken>) -> bool {
|
|||
}
|
||||
|
||||
fn last_dot_before_open_bracket(tokens: &VecDeque<MpsToken>) -> usize {
|
||||
let mut inside_brackets = 0;
|
||||
let mut open_bracket_found = false;
|
||||
for i in (0..tokens.len()).rev() {
|
||||
if tokens[i].is_open_bracket() {
|
||||
open_bracket_found = true;
|
||||
if tokens[i].is_close_bracket() {
|
||||
inside_brackets += 1;
|
||||
} else if tokens[i].is_open_bracket() {
|
||||
if inside_brackets == 1 {
|
||||
open_bracket_found = true;
|
||||
} else if inside_brackets != 0 {
|
||||
inside_brackets -= 1;
|
||||
}
|
||||
} else if open_bracket_found {
|
||||
if tokens[i].is_dot() {
|
||||
return i;
|
||||
|
@ -443,17 +546,55 @@ fn last_dot_before_open_bracket(tokens: &VecDeque<MpsToken>) -> usize {
|
|||
0
|
||||
}
|
||||
|
||||
fn first_double_pipe(tokens: &VecDeque<MpsToken>) -> Option<usize> {
|
||||
fn last_double_pipe(tokens: &VecDeque<MpsToken>, in_brackets: usize) -> Option<usize> {
|
||||
let mut inside_brackets = 0;
|
||||
let mut pipe_found = false;
|
||||
for i in 0..tokens.len() {
|
||||
if tokens[i].is_pipe() {
|
||||
for i in (0..tokens.len()).rev() {
|
||||
if tokens[i].is_pipe() && inside_brackets == in_brackets {
|
||||
if pipe_found {
|
||||
return Some(i-1);
|
||||
return Some(i);
|
||||
} else {
|
||||
pipe_found = true;
|
||||
}
|
||||
} else {
|
||||
pipe_found = false;
|
||||
if tokens[i].is_close_bracket() {
|
||||
inside_brackets += 1;
|
||||
} else if tokens[i].is_open_bracket() && inside_brackets != 0 {
|
||||
inside_brackets -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn first_colon(tokens: &VecDeque<MpsToken>) -> Option<usize> {
|
||||
for i in 0..tokens.len() {
|
||||
if tokens[i].is_colon() {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn first_colon2(tokens: &VecDeque<&MpsToken>) -> Option<usize> {
|
||||
for i in 0..tokens.len() {
|
||||
if tokens[i].is_colon() {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn first_else_not_in_bracket(tokens: &VecDeque<MpsToken>) -> Option<usize> {
|
||||
let mut inside_brackets = 0;
|
||||
for i in 0..tokens.len() {
|
||||
if check_name("else", &tokens[i]) && inside_brackets == 0 {
|
||||
return Some(i);
|
||||
} else if tokens[i].is_open_bracket() {
|
||||
inside_brackets += 1;
|
||||
} else if tokens[i].is_close_bracket() && inside_brackets != 0 {
|
||||
inside_brackets -= 1;
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
292
mps-interpreter/src/lang/filter_replace.rs
Normal file
292
mps-interpreter/src/lang/filter_replace.rs
Normal file
|
@ -0,0 +1,292 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::fmt::{Debug, Display, Error, Formatter};
|
||||
use std::iter::Iterator;
|
||||
|
||||
use crate::lang::{MpsOp, PseudoOp};
|
||||
use crate::lang::RuntimeError;
|
||||
use crate::lang::{MpsFilterPredicate, filter::VariableOrOp};
|
||||
use crate::lang::SingleItem;
|
||||
use crate::processing::general::MpsType;
|
||||
use crate::processing::OpGetter;
|
||||
use crate::MpsContext;
|
||||
use crate::MpsMusicItem;
|
||||
|
||||
const ITEM_VARIABLE_NAME: &str = "item";
|
||||
const ITEM_CACHE_DEFAULT_SIZE: usize = 8;
|
||||
|
||||
#[inline(always)]
|
||||
pub(super) fn item_cache_deque() -> VecDeque<Result<MpsMusicItem, RuntimeError>> {
|
||||
VecDeque::with_capacity(ITEM_CACHE_DEFAULT_SIZE)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MpsFilterReplaceStatement<P: MpsFilterPredicate + 'static> {
|
||||
pub(super) predicate: P,
|
||||
pub(super) iterable: VariableOrOp,
|
||||
pub(super) context: Option<MpsContext>,
|
||||
pub(super) op_if: PseudoOp,
|
||||
pub(super) op_else: Option<PseudoOp>,
|
||||
pub(super) item_cache: VecDeque<Result<MpsMusicItem, RuntimeError>>,
|
||||
}
|
||||
|
||||
impl<P: MpsFilterPredicate + 'static> std::clone::Clone for MpsFilterReplaceStatement<P> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
predicate: self.predicate.clone(),
|
||||
iterable: self.iterable.clone(),
|
||||
context: None,
|
||||
op_if: self.op_if.clone(),
|
||||
op_else: self.op_else.clone(),
|
||||
item_cache: VecDeque::new(), // this doesn't need to be carried around
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: MpsFilterPredicate + 'static> Display for MpsFilterReplaceStatement<P> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
if let Some(op_else) = &self.op_else {
|
||||
write!(f, "{}.(if {}: {} else {})", self.iterable, self.predicate, self.op_if, op_else)
|
||||
} else {
|
||||
write!(f, "{}.(if {}: {})", self.iterable, self.predicate, self.op_if)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: MpsFilterPredicate + 'static> MpsOp for MpsFilterReplaceStatement<P> {
|
||||
fn enter(&mut self, ctx: MpsContext) {
|
||||
self.context = Some(ctx)
|
||||
}
|
||||
|
||||
fn escape(&mut self) -> MpsContext {
|
||||
self.context.take().unwrap()
|
||||
}
|
||||
|
||||
fn is_resetable(&self) -> bool {
|
||||
match &self.iterable {
|
||||
VariableOrOp::Variable(s) => {
|
||||
if self.context.is_some() {
|
||||
let var = self.context.as_ref().unwrap().variables.get_opt(s);
|
||||
if let Some(MpsType::Op(var)) = var {
|
||||
var.is_resetable()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {true} // ASSUMPTION
|
||||
|
||||
}
|
||||
VariableOrOp::Op(PseudoOp::Real(op)) => op.is_resetable(),
|
||||
VariableOrOp::Op(PseudoOp::Fake(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), RuntimeError> {
|
||||
self.item_cache.clear();
|
||||
let fake = PseudoOp::Fake(format!("{}", self));
|
||||
self.predicate.reset()?;
|
||||
match &mut self.iterable {
|
||||
VariableOrOp::Variable(s) => {
|
||||
if self.context.as_mut().unwrap().variables.exists(s) {
|
||||
let fake_getter = &mut move || fake.clone();
|
||||
let mut var = self.context.as_mut().unwrap().variables.remove(s, fake_getter)?;
|
||||
let result = if let MpsType::Op(var) = &mut var {
|
||||
var.enter(self.context.take().unwrap());
|
||||
let result = var.reset();
|
||||
self.context = Some(var.escape());
|
||||
result
|
||||
} else {
|
||||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: fake_getter(),
|
||||
msg: "Cannot reset non-iterable filter variable".to_string(),
|
||||
})
|
||||
};
|
||||
self.context.as_mut().unwrap().variables.declare(s, var, fake_getter)?;
|
||||
result
|
||||
} else {Ok(())}
|
||||
},
|
||||
VariableOrOp::Op(PseudoOp::Real(op)) => {
|
||||
op.enter(self.context.take().unwrap());
|
||||
let result = op.reset();
|
||||
self.context = Some(op.escape());
|
||||
result
|
||||
},
|
||||
VariableOrOp::Op(PseudoOp::Fake(_)) => Err(RuntimeError {
|
||||
line: 0,
|
||||
op: fake,
|
||||
msg: "Cannot reset PseudoOp::Fake filter".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: MpsFilterPredicate + 'static> Iterator for MpsFilterReplaceStatement<P> {
|
||||
type Item = Result<MpsMusicItem, RuntimeError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.item_cache.is_empty() {
|
||||
return self.item_cache.pop_front();
|
||||
}
|
||||
let self_clone = self.clone();
|
||||
let mut op_getter = move || (Box::new(self_clone.clone()) as Box<dyn MpsOp>).into();
|
||||
// get next item in iterator
|
||||
let next_item = match &mut self.iterable {
|
||||
VariableOrOp::Op(op) => match op.try_real() {
|
||||
Ok(real_op) => {
|
||||
let ctx = self.context.take().unwrap();
|
||||
real_op.enter(ctx);
|
||||
let item = real_op.next();
|
||||
self.context = Some(real_op.escape());
|
||||
item
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
||||
},
|
||||
VariableOrOp::Variable(variable_name) => {
|
||||
let mut variable = match self
|
||||
.context
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.variables
|
||||
.remove(&variable_name, &mut op_getter)
|
||||
{
|
||||
Ok(MpsType::Op(op)) => op,
|
||||
Ok(x) => {
|
||||
return Some(Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op_getter(),
|
||||
msg: format!(
|
||||
"Expected operation/iterable type in variable {}, got {}",
|
||||
&variable_name, x
|
||||
),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let ctx = self.context.take().unwrap();
|
||||
variable.enter(ctx);
|
||||
let item = variable.next();
|
||||
self.context = Some(variable.escape());
|
||||
match self.context.as_mut().unwrap().variables.declare(
|
||||
&variable_name,
|
||||
MpsType::Op(variable),
|
||||
&mut op_getter,
|
||||
) {
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(_) => {},
|
||||
}
|
||||
item
|
||||
}
|
||||
};
|
||||
// process item
|
||||
match next_item {
|
||||
Some(Ok(item)) => {
|
||||
//println!("item is now: `{}`", &item.filename);
|
||||
match self.predicate.matches(&item, self.context.as_mut().unwrap(), &mut op_getter) {
|
||||
Ok(is_match) =>
|
||||
if is_match {
|
||||
// unwrap inner operation
|
||||
match self.op_if.try_real() {
|
||||
Ok(real_op) => {
|
||||
// build item variable
|
||||
let single_op = SingleItem::new_ok(item);
|
||||
//println!("Declaring item variable");
|
||||
let old_item = match declare_or_replace_item(single_op, &mut op_getter, self.context.as_mut().unwrap()) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Some(Err(e)), // probably shouldn't occur
|
||||
};
|
||||
// invoke inner op
|
||||
real_op.enter(self.context.take().unwrap());
|
||||
if real_op.is_resetable() {
|
||||
match real_op.reset() {
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
while let Some(item) = real_op.next() {
|
||||
self.item_cache.push_back(item);
|
||||
}
|
||||
self.context = Some(real_op.escape());
|
||||
// destroy item variable
|
||||
//println!("Removing item variable");
|
||||
match remove_or_replace_item(old_item, &mut op_getter, self.context.as_mut().unwrap()) {
|
||||
Ok(_) => {},
|
||||
Err(e) => return Some(Err(e))
|
||||
}
|
||||
}
|
||||
Err(e) => return Some(Err(e)), // probably shouldn't occur
|
||||
}
|
||||
// return cached item, if any
|
||||
let replacement = self.item_cache.pop_front();
|
||||
if replacement.is_none() {
|
||||
self.next()
|
||||
} else {
|
||||
replacement
|
||||
}
|
||||
} else if let Some(op_else) = &mut self.op_else {
|
||||
// unwrap inner operation
|
||||
match op_else.try_real() {
|
||||
Ok(real_op) => {
|
||||
// build item variable
|
||||
let single_op = SingleItem::new_ok(item);
|
||||
//println!("Declaring item variable");
|
||||
let old_item = match declare_or_replace_item(single_op, &mut op_getter, self.context.as_mut().unwrap()) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Some(Err(e)), // probably shouldn't occur
|
||||
};
|
||||
// invoke inner operation
|
||||
real_op.enter(self.context.take().unwrap());
|
||||
if real_op.is_resetable() {
|
||||
match real_op.reset() {
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
while let Some(item) = real_op.next() {
|
||||
self.item_cache.push_back(item);
|
||||
}
|
||||
self.context = Some(real_op.escape());
|
||||
// destroy item variable
|
||||
//println!("Removing item variable");
|
||||
match remove_or_replace_item(old_item, &mut op_getter, self.context.as_mut().unwrap()) {
|
||||
Ok(_) => {},
|
||||
Err(e) => return Some(Err(e))
|
||||
}
|
||||
}
|
||||
Err(e) => return Some(Err(e)), // probably shouldn't occur
|
||||
}
|
||||
// return cached item, if any
|
||||
let replacement = self.item_cache.pop_front();
|
||||
if replacement.is_none() {
|
||||
self.next()
|
||||
} else {
|
||||
replacement
|
||||
}
|
||||
} else {
|
||||
Some(Ok(item))
|
||||
},
|
||||
Err(e) => return Some(Err(e))
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => Some(Err(e)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn declare_or_replace_item(single: SingleItem, op: &mut OpGetter, ctx: &mut MpsContext) -> Result<Option<MpsType>, RuntimeError> {
|
||||
let old_item: Option<MpsType>;
|
||||
if ctx.variables.exists(ITEM_VARIABLE_NAME) {
|
||||
old_item = Some(ctx.variables.remove(ITEM_VARIABLE_NAME, op)?);
|
||||
} else {
|
||||
old_item = None;
|
||||
}
|
||||
ctx.variables.declare(ITEM_VARIABLE_NAME, MpsType::Op(Box::new(single)), op)?;
|
||||
Ok(old_item)
|
||||
}
|
||||
|
||||
fn remove_or_replace_item(old_item: Option<MpsType>, op: &mut OpGetter, ctx: &mut MpsContext) -> Result<(), RuntimeError> {
|
||||
ctx.variables.remove(ITEM_VARIABLE_NAME, op)?;
|
||||
if let Some(old_item) = old_item {
|
||||
ctx.variables.declare(ITEM_VARIABLE_NAME, old_item, op)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -2,6 +2,7 @@ mod db_items;
|
|||
mod dictionary;
|
||||
mod error;
|
||||
mod filter;
|
||||
mod filter_replace;
|
||||
mod function;
|
||||
mod lookup;
|
||||
mod operation;
|
||||
|
@ -17,6 +18,7 @@ pub use error::{MpsLanguageError, RuntimeError, SyntaxError};
|
|||
pub use filter::{
|
||||
MpsFilterFactory, MpsFilterPredicate, MpsFilterStatement, MpsFilterStatementFactory,
|
||||
};
|
||||
pub use filter_replace::MpsFilterReplaceStatement;
|
||||
pub use function::{MpsFunctionFactory, MpsFunctionStatementFactory};
|
||||
pub use lookup::Lookup;
|
||||
pub use operation::{BoxedMpsOpFactory, MpsOp, MpsOpFactory, SimpleMpsOpFactory};
|
||||
|
|
|
@ -121,9 +121,9 @@ pub fn check_is_type(token: &MpsToken) -> bool {
|
|||
match token {
|
||||
MpsToken::Literal(_) => true,
|
||||
MpsToken::Name(s) => {
|
||||
s.parse::<f64>().is_ok()
|
||||
|| s.parse::<i64>().is_ok()
|
||||
s.parse::<i64>().is_ok()
|
||||
|| s.parse::<u64>().is_ok()
|
||||
|| s.parse::<f64>().is_ok()
|
||||
|| s == "false"
|
||||
|| s == "true"
|
||||
}
|
||||
|
@ -143,12 +143,12 @@ pub fn assert_type(tokens: &mut VecDeque<MpsToken>) -> Result<MpsTypePrimitive,
|
|||
match token {
|
||||
MpsToken::Literal(s) => Ok(MpsTypePrimitive::String(s)),
|
||||
MpsToken::Name(s) => {
|
||||
if let Ok(f) = s.parse::<f64>() {
|
||||
Ok(MpsTypePrimitive::Float(f))
|
||||
} else if let Ok(i) = s.parse::<i64>() {
|
||||
if let Ok(i) = s.parse::<i64>() {
|
||||
Ok(MpsTypePrimitive::Int(i))
|
||||
} else if let Ok(u) = s.parse::<u64>() {
|
||||
Ok(MpsTypePrimitive::UInt(u))
|
||||
} else if let Ok(f) = s.parse::<f64>() {
|
||||
Ok(MpsTypePrimitive::Float(f))
|
||||
} else if s == "false" {
|
||||
Ok(MpsTypePrimitive::Bool(false))
|
||||
} else if s == "true" {
|
||||
|
|
|
@ -25,7 +25,11 @@ pub struct RepeatStatement {
|
|||
|
||||
impl Display for RepeatStatement {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
write!(f, "repeat({})", self.inner_statement)
|
||||
if self.loop_forever {
|
||||
write!(f, "repeat({})", self.inner_statement)
|
||||
} else {
|
||||
write!(f, "repeat({}, {})", self.inner_statement, self.original_repetitions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,26 +49,37 @@
|
|||
//! field >= something
|
||||
//! field > something
|
||||
//! field <= something
|
||||
//! field < something -- e.g. iterable.(title == "Romantic Traffic");
|
||||
//! field < something -- e.g. `iterable.(title == "Romantic Traffic");`
|
||||
//!
|
||||
//! Compare all items, keeping only those that match the condition. Valid field names are those of the MpsMusicItem (title, artist, album, genre, track, etc.), though this will change when proper object support is added. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively).
|
||||
//!
|
||||
//! start..end -- e.g. iterable.(0..42);
|
||||
//! start..end -- e.g. `iterable.(0..42);`
|
||||
//!
|
||||
//! Keep only the items that are at the start index up to the end index. Start and/or end may be omitted to start/stop at the iterable's existing start/end (respectively). This stops once the end condition is met, leaving the rest of the iterator unconsumed.
|
||||
//!
|
||||
//! start..=end -- e.g. iterable.(0..=42);
|
||||
//! start..=end -- e.g. `iterable.(0..=42);`
|
||||
//!
|
||||
//! Keep only the items that are at the start index up to and including the end index. Start may be omitted to start at the iterable's existing start. This stops once the end condition is met, leaving the rest of the iterator unconsumed.
|
||||
//!
|
||||
//! index -- e.g. iterable.(4);
|
||||
//! index -- e.g. `iterable.(4);`
|
||||
//!
|
||||
//! Keep only the item at the given index. This stops once the index is reached, leaving the rest of the iterator unconsumed.
|
||||
//!
|
||||
//! predicate1 || predicate2 -- e.g. iterable.(4 || 5);
|
||||
//! predicate1 || predicate2 -- e.g. `iterable.(4 || 5);`
|
||||
//!
|
||||
//! Keep only the items that meet the criteria of predicate1 or predicate2. This will always consume the full iterator.
|
||||
//!
|
||||
//! [empty] -- e.g. iterable.();
|
||||
//! [empty] -- e.g. `iterable.();`
|
||||
//!
|
||||
//! Matches all items
|
||||
//!
|
||||
//! if filter: operation1 else operation2 -- e.g. `iterable.(if title == "Romantic Traffic": repeat(item, 2) else item.());`
|
||||
//!
|
||||
//! Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`.
|
||||
//!
|
||||
//! ## Functions
|
||||
//! Similar to most other languages: `function_name(param1, param2, etc.);`.
|
||||
//! These always return an iterable which can me manipulated.
|
||||
//! Functions are statements of the format `function_name(params)`, where "function_name" is one of the function names (below) and params is a valid parameter input for the function.
|
||||
//! Each function is responsible for parsing it's own parameters when the statement is parsed, so this is very flexible.
|
||||
//! E.g. `files(folder="~/Music/", recursive=true);` is valid function syntax to execute the files function with parameters `folder="~/Music/", recursive=true`.
|
||||
|
|
|
@ -30,7 +30,7 @@ pub trait MpsVariableStorer: Debug {
|
|||
None => Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op(),
|
||||
msg: format!("Variable {} not found", name),
|
||||
msg: format!("Variable '{}' not found", name),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ pub trait MpsVariableStorer: Debug {
|
|||
None => Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op(),
|
||||
msg: format!("Variable {} not found", name),
|
||||
msg: format!("Variable '{}' not found", name),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,10 @@ pub trait MpsVariableStorer: Debug {
|
|||
) -> Result<(), RuntimeError>;
|
||||
|
||||
fn remove(&mut self, name: &str, op: &mut OpGetter) -> Result<MpsType, RuntimeError>;
|
||||
|
||||
fn exists(&self, name: &str) -> bool {
|
||||
self.get_opt(name).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
@ -82,7 +86,7 @@ impl MpsVariableStorer for MpsOpStorage {
|
|||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op(),
|
||||
msg: format!("Cannot assign to non-existent variable {}", key),
|
||||
msg: format!("Cannot assign to non-existent variable '{}'", key),
|
||||
})
|
||||
} else {
|
||||
self.storage.insert(key.to_string(), item);
|
||||
|
@ -95,7 +99,7 @@ impl MpsVariableStorer for MpsOpStorage {
|
|||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op(),
|
||||
msg: format!("Cannot overwrite existing variable {}", key),
|
||||
msg: format!("Cannot overwrite existing variable '{}'", key),
|
||||
})
|
||||
} else {
|
||||
self.storage.insert(key.to_string(), item);
|
||||
|
@ -110,7 +114,7 @@ impl MpsVariableStorer for MpsOpStorage {
|
|||
Err(RuntimeError {
|
||||
line: 0,
|
||||
op: op(),
|
||||
msg: format!("Cannot remove non-existing variable {}", key),
|
||||
msg: format!("Cannot remove non-existing variable '{}'", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ pub enum MpsToken {
|
|||
Interrogation,
|
||||
Pipe,
|
||||
Ampersand,
|
||||
Colon,
|
||||
}
|
||||
|
||||
impl MpsToken {
|
||||
|
@ -38,6 +39,7 @@ impl MpsToken {
|
|||
"?" => Ok(Self::Interrogation),
|
||||
"|" => Ok(Self::Pipe),
|
||||
"&" => Ok(Self::Ampersand),
|
||||
":" => Ok(Self::Colon),
|
||||
_ => {
|
||||
// name validation
|
||||
let mut ok = true;
|
||||
|
@ -174,6 +176,13 @@ impl MpsToken {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_colon(&self) -> bool {
|
||||
match self {
|
||||
Self::Colon => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MpsToken {
|
||||
|
@ -196,6 +205,7 @@ impl Display for MpsToken {
|
|||
Self::Interrogation => write!(f, "?"),
|
||||
Self::Pipe => write!(f, "|"),
|
||||
Self::Ampersand => write!(f, "&"),
|
||||
Self::Colon => write!(f, ":")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ impl ReaderStateMachine {
|
|||
'\n'| '\r' | '\t' | ' ' => Self::EndToken {},
|
||||
';' => Self::EndStatement {},
|
||||
'\0' => Self::EndOfFile {},
|
||||
'(' | ')' | ',' | '=' | '<' | '>' | '.' | '!' | '?' | '|' => Self::SingleCharToken { out: input },
|
||||
'(' | ')' | ',' | '=' | '<' | '>' | '.' | '!' | '?' | '|' | ':' => Self::SingleCharToken { out: input },
|
||||
_ => Self::Regular { out: input },
|
||||
},
|
||||
Self::Escaped { inside } => match inside {
|
||||
|
|
|
@ -132,7 +132,7 @@ fn execute_assign_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
|||
|
||||
#[test]
|
||||
fn execute_emptyfilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
execute_single_line("song(`lov`).()", false, true)
|
||||
execute_single_line("files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).().().()", false, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -264,3 +264,22 @@ fn execute_orfilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
|||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_replacefilter_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
execute_single_line(
|
||||
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(5))",
|
||||
false,
|
||||
true,
|
||||
)?;
|
||||
execute_single_line(
|
||||
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(5) else item.())",
|
||||
false,
|
||||
true,
|
||||
)?;
|
||||
execute_single_line(
|
||||
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(if 4: item.() else files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(0 || 1).(if 200: files() else repeat(item.(), 2)))",
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ To view the currently-supported operations, try ?functions and ?filters";
|
|||
pub const FUNCTIONS: &str =
|
||||
"FUNCTIONS (?functions)
|
||||
Similar to most other languages: function_name(param1, param2, etc.)
|
||||
These always return an iterable which can me manipulated.
|
||||
|
||||
sql_init(generate = true|false, folder = `path/to/music`)
|
||||
Initialize the SQLite database connection using the provided parameters. This must be performed before any other database operation (otherwise the database will already be connected with default settings).
|
||||
|
@ -57,4 +58,7 @@ Operations to reduce the items in an iterable: iterable.(filter)
|
|||
Keep only the items that meet the criteria of filter1 or filter2. This will always consume the full iterator.
|
||||
|
||||
[empty] -- e.g. iterable.()
|
||||
Matches all items";
|
||||
Matches all items
|
||||
|
||||
if filter: operation1 else operation2 -- e.g. iterable.(if title == `Romantic Traffic`: repeat(item, 2) else item.())
|
||||
Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`.";
|
||||
|
|
Loading…
Reference in a new issue