Add all replacement filter functionality

This commit is contained in:
NGnius (Graham) 2022-01-18 09:14:05 -05:00
parent dd5f15c745
commit b9def2f15c
12 changed files with 588 additions and 90 deletions

View file

@ -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`.

View file

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

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

View file

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

View file

@ -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" {

View file

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

View file

@ -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`.

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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`.";