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#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParsedQotSymbol {
22 pub market: i32,
23 pub code: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum QotSymbolParseError {
28 SymbolTooLong { len: usize },
29 MissingSeparator { raw: String },
30 EmptyCode { raw: String },
31 CodeTooLong { len: usize },
32 UnknownMarket { raw: String, market: String },
33}
34
35impl fmt::Display for QotSymbolParseError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::SymbolTooLong { len } => write!(
39 f,
40 "invalid symbol: symbol length {len} exceeds max {MAX_SYMBOL_LEN}"
41 ),
42 Self::MissingSeparator { raw } => write!(
43 f,
44 "invalid symbol {raw:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)"
45 ),
46 Self::EmptyCode { raw } => {
47 write!(f, "invalid symbol {raw:?}: code part is empty")
48 }
49 Self::CodeTooLong { len } => write!(
50 f,
51 "invalid symbol code: code length {len} exceeds max {MAX_SYMBOL_CODE_LEN}"
52 ),
53 Self::UnknownMarket { raw, market } => write!(
54 f,
55 "invalid symbol {raw:?}: unknown market {market:?} (supported: {SUPPORTED_SYMBOL_MARKETS})"
56 ),
57 }
58 }
59}
60
61impl std::error::Error for QotSymbolParseError {}
62
63pub fn parse_qot_symbol_parts(raw: &str) -> Result<ParsedQotSymbol, QotSymbolParseError> {
64 validate_full_symbol_len(raw)?;
65 let (market_str, code) =
66 raw.split_once('.')
67 .ok_or_else(|| QotSymbolParseError::MissingSeparator {
68 raw: raw.to_string(),
69 })?;
70 if code.is_empty() {
71 return Err(QotSymbolParseError::EmptyCode {
72 raw: raw.to_string(),
73 });
74 }
75 validate_symbol_code_len(code)?;
76
77 let market_upper = market_str.to_ascii_uppercase();
78 let market = match market_upper.as_str() {
79 "HK" => 1,
80 "HK_FUTURE" => 2,
81 "US" => 11,
82 "SH" => 21,
83 "SZ" => 22,
84 "SG" => 31,
85 "JP" => 41,
86 "AU" => 51,
87 "MY" => 61,
88 "CA" => 71,
89 "FX" => 81,
90 "CRYPTO" | "CC" => 91,
91 "EC" => 101,
93 _ => {
94 return Err(QotSymbolParseError::UnknownMarket {
95 raw: raw.to_string(),
96 market: market_upper,
97 });
98 }
99 };
100
101 Ok(ParsedQotSymbol {
102 market,
103 code: code.to_string(),
104 })
105}
106
107#[must_use]
108pub fn qot_market_from_str_alias(raw: &str) -> Option<i32> {
109 Some(match raw.trim().to_ascii_uppercase().as_str() {
110 "HK" => 1,
111 "HK_FUTURE" | "HKFUTURE" => 2,
112 "US" => 11,
113 "CN" | "SH" => 21,
114 "SZ" => 22,
115 "SG" => 31,
116 "JP" => 41,
117 "AU" => 51,
118 "MY" => 61,
119 "CA" => 71,
120 "FX" => 81,
121 "CRYPTO" | "CC" => 91,
122 "EC" | "EVENT_CONTRACT" | "EVENTCONTRACT" => 101,
123 _ => return None,
124 })
125}
126
127pub fn validate_full_symbol_len(raw: &str) -> Result<(), QotSymbolParseError> {
128 if raw.len() > MAX_SYMBOL_LEN {
129 return Err(QotSymbolParseError::SymbolTooLong { len: raw.len() });
130 }
131 Ok(())
132}
133
134pub fn validate_symbol_code_len(code: &str) -> Result<(), QotSymbolParseError> {
135 if code.len() > MAX_SYMBOL_CODE_LEN {
136 return Err(QotSymbolParseError::CodeTooLong { len: code.len() });
137 }
138 Ok(())
139}
140
141#[must_use]
142pub fn format_qot_symbol(market: i32, code: &str) -> String {
143 let prefix = qot_market_display_prefix(QotMarketId::new(market)).unwrap_or("UNKNOWN");
144 format!("{prefix}.{code}")
145}