use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Field {
IntradayPrice,
PercentChange,
PriceChange,
DayVolume,
AvgDailyVolume3Month,
AvgDailyVolume10Day,
IntradayMarketCap,
PERatioTTM,
PERatioForward,
PEGRatio5Y,
PriceToBook,
PriceToSales,
EPSGrowthTTM,
EPSGrowthQuarterlyYoY,
RevenueGrowthTTM,
ProfitMargin,
OperatingMargin,
ReturnOnEquity,
ReturnOnAssets,
DividendYield,
TrailingAnnualDividendRate,
TrailingAnnualDividendYield,
FiftyTwoWeekHigh,
FiftyTwoWeekLow,
PercentFromFiftyTwoWeekHigh,
PercentFromFiftyTwoWeekLow,
Beta,
Region,
Sector,
Industry,
Exchange,
QuoteType,
}
impl Field {
pub fn yahoo_name(&self) -> &'static str {
match self {
Field::IntradayPrice => "intradayprice",
Field::PercentChange => "percentchange",
Field::PriceChange => "pricechange",
Field::DayVolume => "dayvolume",
Field::AvgDailyVolume3Month => "avgdailyvol3m",
Field::AvgDailyVolume10Day => "avgdailyvol10d",
Field::IntradayMarketCap => "intradaymarketcap",
Field::PERatioTTM => "peratio.lasttwelvemonths",
Field::PERatioForward => "peratio.forward",
Field::PEGRatio5Y => "pegratio_5y",
Field::PriceToBook => "pricetobook",
Field::PriceToSales => "pricetosales",
Field::EPSGrowthTTM => "epsgrowth.lasttwelvemonths",
Field::EPSGrowthQuarterlyYoY => "epsgrowth.quarterly.yoy",
Field::RevenueGrowthTTM => "revenuegrowth.lasttwelvemonths",
Field::ProfitMargin => "profitmargin",
Field::OperatingMargin => "operatingmargin",
Field::ReturnOnEquity => "returnonequity",
Field::ReturnOnAssets => "returnonassets",
Field::DividendYield => "dividendyield",
Field::TrailingAnnualDividendRate => "trailingannualdividendrate",
Field::TrailingAnnualDividendYield => "trailingannualdividendyield",
Field::FiftyTwoWeekHigh => "fiftytwoweek.high",
Field::FiftyTwoWeekLow => "fiftytwoweek.low",
Field::PercentFromFiftyTwoWeekHigh => "percentfromfiftytwoweek.high",
Field::PercentFromFiftyTwoWeekLow => "percentfromfiftytwoweek.low",
Field::Beta => "beta",
Field::Region => "region",
Field::Sector => "sector",
Field::Industry => "industry",
Field::Exchange => "exchange",
Field::QuoteType => "quotetype",
}
}
pub fn is_numeric(&self) -> bool {
!matches!(
self,
Field::Region | Field::Sector | Field::Industry | Field::Exchange | Field::QuoteType
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operator {
And,
Or,
GreaterThan,
LessThan,
GreaterThanOrEqual,
LessThanOrEqual,
Equal,
Between,
In,
}
impl Operator {
pub fn yahoo_name(&self) -> &'static str {
match self {
Operator::And => "and",
Operator::Or => "or",
Operator::GreaterThan => "gt",
Operator::LessThan => "lt",
Operator::GreaterThanOrEqual => "gte",
Operator::LessThanOrEqual => "lte",
Operator::Equal => "eq",
Operator::Between => "btwn",
Operator::In => "in",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum QueryValue {
String(String),
Number(f64),
Integer(i64),
Array(Vec<QueryValue>),
}
impl From<&str> for QueryValue {
fn from(s: &str) -> Self {
QueryValue::String(s.to_string())
}
}
impl From<String> for QueryValue {
fn from(s: String) -> Self {
QueryValue::String(s)
}
}
impl From<f64> for QueryValue {
fn from(n: f64) -> Self {
QueryValue::Number(n)
}
}
impl From<i64> for QueryValue {
fn from(n: i64) -> Self {
QueryValue::Integer(n)
}
}
impl From<Vec<QueryValue>> for QueryValue {
fn from(arr: Vec<QueryValue>) -> Self {
QueryValue::Array(arr)
}
}
#[derive(Debug, Clone)]
pub struct Query {
operator: Operator,
operands: Vec<QueryOperand>,
}
#[derive(Debug, Clone)]
enum QueryOperand {
Field(Field),
Value(QueryValue),
NestedQuery(Box<Query>),
}
impl Query {
pub fn and(queries: Vec<Query>) -> Self {
Self {
operator: Operator::And,
operands: queries.into_iter().map(|q| QueryOperand::NestedQuery(Box::new(q))).collect(),
}
}
pub fn or(queries: Vec<Query>) -> Self {
Self {
operator: Operator::Or,
operands: queries.into_iter().map(|q| QueryOperand::NestedQuery(Box::new(q))).collect(),
}
}
pub fn eq<V: Into<QueryValue>>(field: Field, value: V) -> Self {
Self {
operator: Operator::Equal,
operands: vec![QueryOperand::Field(field), QueryOperand::Value(value.into())],
}
}
pub fn gt<V: Into<QueryValue>>(field: Field, value: V) -> Self {
Self {
operator: Operator::GreaterThan,
operands: vec![QueryOperand::Field(field), QueryOperand::Value(value.into())],
}
}
pub fn lt<V: Into<QueryValue>>(field: Field, value: V) -> Self {
Self {
operator: Operator::LessThan,
operands: vec![QueryOperand::Field(field), QueryOperand::Value(value.into())],
}
}
pub fn gte<V: Into<QueryValue>>(field: Field, value: V) -> Self {
Self {
operator: Operator::GreaterThanOrEqual,
operands: vec![QueryOperand::Field(field), QueryOperand::Value(value.into())],
}
}
pub fn lte<V: Into<QueryValue>>(field: Field, value: V) -> Self {
Self {
operator: Operator::LessThanOrEqual,
operands: vec![QueryOperand::Field(field), QueryOperand::Value(value.into())],
}
}
pub fn between<V1: Into<QueryValue>, V2: Into<QueryValue>>(
field: Field,
min: V1,
max: V2,
) -> Self {
Self {
operator: Operator::Between,
operands: vec![
QueryOperand::Field(field),
QueryOperand::Value(QueryValue::Array(vec![min.into(), max.into()])),
],
}
}
pub fn in_list(field: Field, values: Vec<QueryValue>) -> Self {
Self {
operator: Operator::In,
operands: vec![
QueryOperand::Field(field),
QueryOperand::Value(QueryValue::Array(values)),
],
}
}
pub(crate) fn to_json(&self) -> JsonValue {
let operator_name = self.operator.yahoo_name();
let operands: Vec<JsonValue> = self
.operands
.iter()
.map(|operand| match operand {
QueryOperand::Field(field) => JsonValue::String(field.yahoo_name().to_string()),
QueryOperand::Value(value) => serde_json::to_value(value).unwrap(),
QueryOperand::NestedQuery(query) => query.to_json(),
})
.collect();
serde_json::json!({
"operator": operator_name,
"operands": operands,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_yahoo_names() {
assert_eq!(Field::IntradayPrice.yahoo_name(), "intradayprice");
assert_eq!(Field::PercentChange.yahoo_name(), "percentchange");
assert_eq!(Field::IntradayMarketCap.yahoo_name(), "intradaymarketcap");
}
#[test]
fn test_field_is_numeric() {
assert!(Field::IntradayPrice.is_numeric());
assert!(Field::PercentChange.is_numeric());
assert!(!Field::Region.is_numeric());
assert!(!Field::Sector.is_numeric());
}
#[test]
fn test_operator_yahoo_names() {
assert_eq!(Operator::And.yahoo_name(), "and");
assert_eq!(Operator::GreaterThan.yahoo_name(), "gt");
assert_eq!(Operator::Equal.yahoo_name(), "eq");
}
#[test]
fn test_query_value_conversions() {
let v1: QueryValue = "test".into();
assert!(matches!(v1, QueryValue::String(_)));
let v2: QueryValue = 42.5.into();
assert!(matches!(v2, QueryValue::Number(_)));
let v3: QueryValue = 100i64.into();
assert!(matches!(v3, QueryValue::Integer(_)));
}
#[test]
fn test_simple_query_to_json() {
let query = Query::eq(Field::Region, "us");
let json = query.to_json();
assert_eq!(json["operator"], "eq");
assert_eq!(json["operands"][0], "region");
assert_eq!(json["operands"][1], "us");
}
#[test]
fn test_comparison_query_to_json() {
let query = Query::gt(Field::PercentChange, 5.0);
let json = query.to_json();
assert_eq!(json["operator"], "gt");
assert_eq!(json["operands"][0], "percentchange");
assert_eq!(json["operands"][1], 5.0);
}
#[test]
fn test_between_query_to_json() {
let query = Query::between(Field::IntradayPrice, 10.0, 50.0);
let json = query.to_json();
assert_eq!(json["operator"], "btwn");
assert_eq!(json["operands"][0], "intradayprice");
assert!(json["operands"][1].is_array());
}
#[test]
fn test_and_query_to_json() {
let query = Query::and(vec![
Query::eq(Field::Region, "us"),
Query::gt(Field::PercentChange, 3.0),
]);
let json = query.to_json();
assert_eq!(json["operator"], "and");
assert!(json["operands"].is_array());
assert_eq!(json["operands"].as_array().unwrap().len(), 2);
}
#[test]
fn test_complex_nested_query() {
let query = Query::and(vec![
Query::eq(Field::Region, "us"),
Query::or(vec![
Query::eq(Field::Sector, "Technology"),
Query::eq(Field::Sector, "Healthcare"),
]),
Query::gt(Field::IntradayMarketCap, 1_000_000_000.0),
]);
let json = query.to_json();
assert_eq!(json["operator"], "and");
assert_eq!(json["operands"].as_array().unwrap().len(), 3);
let nested_or = &json["operands"][1];
assert_eq!(nested_or["operator"], "or");
}
}