1use crate::error::{Result, TvscreenerError};
7use crate::field::{
8 all_index_symbols, all_markets, index_symbol_value, market, resolve_country, resolve_exchange,
9 resolve_industry, resolve_sector,
10};
11
12#[must_use]
14pub fn parse_csv_tokens(raw: Option<&str>) -> Vec<String> {
15 raw.map(|s| {
16 s.split(',')
17 .map(str::trim)
18 .filter(|p| !p.is_empty())
19 .map(str::to_string)
20 .collect()
21 })
22 .unwrap_or_default()
23}
24
25pub fn resolve_index_wires(indices: Option<&str>) -> Result<Vec<String>> {
31 resolve_token_csv(indices, "index", |token| {
32 let catalog = token.strip_prefix("SYML:").unwrap_or(token);
33 let upper = catalog.to_ascii_uppercase().replace(' ', "_");
34 if let Some(wire) = index_symbol_value(&upper) {
35 return Some(wire.to_string());
36 }
37 all_index_symbols()
38 .iter()
39 .find(|idx| idx.value.eq_ignore_ascii_case(catalog))
40 .map(|idx| idx.value.clone())
41 })
42}
43
44pub fn resolve_market_wires(markets: Option<&str>) -> Result<Vec<String>> {
50 resolve_token_csv(markets, "market", |token| {
51 let upper = token.to_ascii_uppercase().replace(' ', "_");
52 if let Some(m) = market(&upper) {
53 return Some(m.value);
54 }
55 all_markets()
56 .iter()
57 .find(|m| m.value.eq_ignore_ascii_case(token))
58 .map(|m| m.value.clone())
59 })
60}
61
62pub fn resolve_sector_wires(sectors: Option<&str>) -> Result<Vec<String>> {
68 resolve_named_csv(sectors, resolve_sector, "sector")
69}
70
71pub fn resolve_country_wires(countries: Option<&str>) -> Result<Vec<String>> {
77 resolve_named_csv(countries, resolve_country, "country")
78}
79
80pub fn resolve_industry_wires(industries: Option<&str>) -> Result<Vec<String>> {
86 resolve_named_csv(industries, resolve_industry, "industry")
87}
88
89pub fn resolve_exchange_wires(exchanges: Option<&str>) -> Result<Vec<String>> {
95 resolve_named_csv(exchanges, resolve_exchange, "exchange")
96}
97
98pub fn apply_stock_index_markets(
104 screener: &mut crate::core::Screener,
105 indices: Option<&str>,
106 markets: Option<&str>,
107) -> Result<()> {
108 let wires = resolve_index_wires(indices)?;
109 if !wires.is_empty() {
110 screener.set_index(wires);
111 }
112 let market_wires = resolve_market_wires(markets)?;
113 if !market_wires.is_empty() {
114 screener.set_markets(market_wires);
115 }
116 Ok(())
117}
118
119fn resolve_token_csv(
120 raw: Option<&str>,
121 kind: &str,
122 resolve_one: impl Fn(&str) -> Option<String>,
123) -> Result<Vec<String>> {
124 let mut out = Vec::new();
125 for token in parse_csv_tokens(raw) {
126 let Some(wire) = resolve_one(&token) else {
127 return Err(TvscreenerError::InvalidRequest(format!(
128 "unknown {kind} `{token}`"
129 )));
130 };
131 out.push(wire);
132 }
133 Ok(out)
134}
135
136fn resolve_named_csv(
137 raw: Option<&str>,
138 resolve: fn(&str) -> Option<&'static str>,
139 kind: &str,
140) -> Result<Vec<String>> {
141 resolve_token_csv(raw, kind, |token| resolve(token).map(str::to_string))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn market_and_index_wires() {
150 assert_eq!(
151 resolve_market_wires(Some("AMERICA")).unwrap(),
152 vec!["america".to_string()]
153 );
154 assert_eq!(
155 resolve_index_wires(Some("SP500")).unwrap(),
156 vec!["SP;SPX".to_string()]
157 );
158 assert!(resolve_market_wires(Some("not-a-market")).is_err());
159 }
160
161 #[test]
162 fn named_catalog_wires() {
163 assert_eq!(
164 resolve_sector_wires(Some("TECHNOLOGY_SERVICES")).unwrap(),
165 vec!["Technology Services".to_string()]
166 );
167 assert_eq!(
168 resolve_country_wires(Some("UNITED_STATES")).unwrap(),
169 vec!["United States".to_string()]
170 );
171 assert_eq!(
172 resolve_industry_wires(Some("SEMICONDUCTORS")).unwrap(),
173 vec!["Semiconductors".to_string()]
174 );
175 assert_eq!(
176 resolve_exchange_wires(Some("NASDAQ")).unwrap(),
177 vec!["NASDAQ".to_string()]
178 );
179 assert!(resolve_sector_wires(Some("nope")).is_err());
180 assert!(resolve_country_wires(Some("nope")).is_err());
181 }
182
183 #[test]
184 fn parse_csv_and_apply_stock_wires() {
185 assert_eq!(
186 parse_csv_tokens(Some(" AMERICA , EUROPE ")),
187 vec!["AMERICA".to_string(), "EUROPE".to_string()]
188 );
189 let mut s = crate::core::Screener::new("global");
190 s.select([crate::field::FieldDef::new("Name", "name")]);
191 apply_stock_index_markets(&mut s, Some("SP500"), Some("AMERICA")).unwrap();
192 let payload = s.build_payload().unwrap();
193 assert_eq!(
194 payload["symbols"]["symbolset"],
195 serde_json::json!(["SYML:SP;SPX"])
196 );
197 assert_eq!(payload["markets"], serde_json::json!(["america"]));
198 }
199}