1use std::fmt;
4
5use crate::market::{QotMarketId, qot_market_display_prefix};
6
7const SUPPORTED_SYMBOL_MARKETS: &str =
8 "HK, HK_FUTURE, US, SH, SZ, SG, JP, AU, MY, CA, FX, CRYPTO, CC, EC";
9
10pub const MAX_SYMBOL_LEN: usize = 160;
18pub const MAX_SYMBOL_CODE_LEN: usize = 128;
19
20#[must_use]
25pub fn looks_like_cpp_option_code(code: &str) -> bool {
26 let bytes = code.as_bytes();
27 if bytes.len() < 10 || !bytes.iter().all(u8::is_ascii) {
28 return false;
29 }
30 (7..bytes.len().saturating_sub(1)).any(|marker| {
31 let prefix_end = marker - 6;
32 prefix_end > 0
33 && matches!(bytes[marker], b'P' | b'p' | b'C' | b'c')
34 && bytes[..prefix_end]
35 .iter()
36 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_'))
37 && bytes[prefix_end..marker].iter().all(u8::is_ascii_digit)
38 && bytes[marker + 1..].len() >= 2
39 && bytes[marker + 1..].iter().all(u8::is_ascii_digit)
40 })
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParsedQotSymbol {
45 pub market: i32,
46 pub code: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum QotSymbolParseError {
51 SymbolTooLong { len: usize },
52 MissingSeparator { raw: String },
53 EmptyCode { raw: String },
54 CodeTooLong { len: usize },
55 UnknownMarket { raw: String, market: String },
56}
57
58impl fmt::Display for QotSymbolParseError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::SymbolTooLong { len } => write!(
62 f,
63 "invalid symbol: symbol length {len} exceeds max {MAX_SYMBOL_LEN}"
64 ),
65 Self::MissingSeparator { raw } => write!(
66 f,
67 "invalid symbol {raw:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)"
68 ),
69 Self::EmptyCode { raw } => {
70 write!(f, "invalid symbol {raw:?}: code part is empty")
71 }
72 Self::CodeTooLong { len } => write!(
73 f,
74 "invalid symbol code: code length {len} exceeds max {MAX_SYMBOL_CODE_LEN}"
75 ),
76 Self::UnknownMarket { raw, market } => write!(
77 f,
78 "invalid symbol {raw:?}: unknown market {market:?} (supported: {SUPPORTED_SYMBOL_MARKETS})"
79 ),
80 }
81 }
82}
83
84impl std::error::Error for QotSymbolParseError {}
85
86pub fn parse_qot_symbol_parts(raw: &str) -> Result<ParsedQotSymbol, QotSymbolParseError> {
87 validate_full_symbol_len(raw)?;
88 let (market_str, code) =
89 raw.split_once('.')
90 .ok_or_else(|| QotSymbolParseError::MissingSeparator {
91 raw: raw.to_string(),
92 })?;
93 if code.is_empty() {
94 return Err(QotSymbolParseError::EmptyCode {
95 raw: raw.to_string(),
96 });
97 }
98 validate_symbol_code_len(code)?;
99
100 let market_upper = market_str.to_ascii_uppercase();
101 let market = match market_upper.as_str() {
102 "HK" => 1,
103 "HK_FUTURE" => 2,
104 "US" => 11,
105 "SH" => 21,
106 "SZ" => 22,
107 "SG" => 31,
108 "JP" => 41,
109 "AU" => 51,
110 "MY" => 61,
111 "CA" => 71,
112 "FX" => 81,
113 "CRYPTO" | "CC" => 91,
114 "EC" => 101,
116 _ => {
117 return Err(QotSymbolParseError::UnknownMarket {
118 raw: raw.to_string(),
119 market: market_upper,
120 });
121 }
122 };
123
124 Ok(ParsedQotSymbol {
125 market,
126 code: code.to_string(),
127 })
128}
129
130#[must_use]
131pub fn qot_market_from_str_alias(raw: &str) -> Option<i32> {
132 Some(match raw.trim().to_ascii_uppercase().as_str() {
133 "HK" => 1,
134 "HK_FUTURE" | "HKFUTURE" => 2,
135 "US" => 11,
136 "CN" | "SH" => 21,
137 "SZ" => 22,
138 "SG" => 31,
139 "JP" => 41,
140 "AU" => 51,
141 "MY" => 61,
142 "CA" => 71,
143 "FX" => 81,
144 "CRYPTO" | "CC" => 91,
145 "EC" | "EVENT_CONTRACT" | "EVENTCONTRACT" => 101,
146 _ => return None,
147 })
148}
149
150pub fn validate_full_symbol_len(raw: &str) -> Result<(), QotSymbolParseError> {
151 if raw.len() > MAX_SYMBOL_LEN {
152 return Err(QotSymbolParseError::SymbolTooLong { len: raw.len() });
153 }
154 Ok(())
155}
156
157pub fn validate_symbol_code_len(code: &str) -> Result<(), QotSymbolParseError> {
158 if code.len() > MAX_SYMBOL_CODE_LEN {
159 return Err(QotSymbolParseError::CodeTooLong { len: code.len() });
160 }
161 Ok(())
162}
163
164#[must_use]
165pub fn format_qot_symbol(market: i32, code: &str) -> String {
166 let prefix = qot_market_display_prefix(QotMarketId::new(market)).unwrap_or("UNKNOWN");
167 format!("{prefix}.{code}")
168}