mod lex;
mod ops;
mod parse;
mod print;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub line: usize,
pub col: usize,
pub message: String,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}: {}", self.line, self.col, self.message)
}
}
impl std::error::Error for ParseError {}
pub use parse::parse;
pub use print::format;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_error_displays_with_position() {
let e = ParseError {
line: 3,
col: 7,
message: "bad token".into(),
};
assert_eq!(e.to_string(), "3:7: bad token");
}
}
#[cfg(test)]
mod roundtrip {
use super::{format, parse};
use crate::spec::Expr;
use serde_json::{json, Value};
fn corpus() -> Vec<Value> {
vec![
json!({"op":"Gt","l":{"op":"Data","name":"close"},
"r":{"op":"Average","of":{"op":"Data","name":"close"},"n":2}}),
json!({"op":"Rank","of":{"op":"Neg","of":{"op":"Data","name":"pe"}}}),
json!({"op":"Vwap","high":{"op":"Data","name":"high"},"low":{"op":"Data","name":"low"},
"close":{"op":"Data","name":"close"},"volume":{"op":"Data","name":"volume"},"n":20}),
json!({"op":"Mask","of":{"op":"IsLargest","of":{"op":"Data","name":"x"},"n":30},
"by":{"op":"Gt","l":{"op":"Data","name":"market_cap"},"r":{"op":"Const","value":500000000}}}),
json!({"op":"Sustain","of":{"op":"Data","name":"x"},"nwindow":3,"nsatisfy":2}),
json!({"op":"Rebalance","of":{"op":"Data","name":"x"},"freq":"ME"}),
json!({"op":"Neutralize","of":{"op":"Data","name":"x"},
"by":[{"op":"Data","name":"pe"},{"op":"Data","name":"market_cap"}]}),
json!({"op":"NeutralizeIndustry","of":{"op":"Data","name":"x"}}),
json!({"op":"HoldUntil",
"entry":{"op":"Gt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":1}},
"exit":{"op":"Lt","l":{"op":"Data","name":"close"},"r":{"op":"Const","value":1}},
"nstocks_limit":1}),
json!({"op":"Add",
"l":{"op":"Mul","l":{"op":"Const","value":2},"r":{"op":"Data","name":"x"}},
"r":{"op":"Data","name":"y"}}),
json!({"op":"IndustryRank","of":{"op":"Data","name":"x"},
"categories":["tech","fin"]}),
]
}
#[test]
fn corpus_is_valid_expr() {
for tree in corpus() {
let r: Result<Expr, _> = serde_json::from_value(tree.clone());
assert!(r.is_ok(), "not a valid Expr: {tree}");
}
}
#[test]
fn parse_of_print_is_identity() {
for tree in corpus() {
let text = format(&tree);
let back = parse(&text).unwrap_or_else(|e| panic!("reparse failed for `{text}`: {e}"));
assert_eq!(back, tree, "round-trip mismatch via `{text}`");
}
}
#[test]
fn print_is_idempotent() {
for tree in corpus() {
let once = format(&tree);
let reparsed = parse(&once).unwrap();
assert_eq!(
format(&reparsed),
once,
"format not idempotent for `{once}`"
);
}
}
}