use crate::types::MarketKind;
#[must_use]
pub fn detect_market(symbol: &str) -> MarketKind {
if normalize_a_share_symbol(symbol).is_some() {
return MarketKind::AShare;
}
if normalize_hk_symbol(symbol).is_some() {
return MarketKind::HongKong;
}
MarketKind::UsEquity
}
#[must_use]
pub fn normalize_a_share_symbol(symbol: &str) -> Option<String> {
let trimmed = symbol.trim();
if let Some((code, suffix)) = trimmed.split_once('.') {
let suffix_upper = suffix.to_uppercase();
if matches!(suffix_upper.as_str(), "SH" | "SZ" | "BJ")
&& code.len() == 6
&& code.chars().all(|c| c.is_ascii_digit())
{
return Some(format!("{code}.{suffix_upper}"));
}
}
if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
let suffix = infer_a_share_exchange(trimmed)?;
return Some(format!("{trimmed}.{suffix}"));
}
let lower = trimmed.to_lowercase();
if lower.starts_with("sh") && lower.len() == 8 {
let code = &lower[2..];
if code.chars().all(|c| c.is_ascii_digit()) && code.len() == 6 {
return Some(format!("{code}.SH"));
}
}
if lower.starts_with("sz") && lower.len() == 8 {
let code = &lower[2..];
if code.chars().all(|c| c.is_ascii_digit()) && code.len() == 6 {
return Some(format!("{code}.SZ"));
}
}
None
}
fn infer_a_share_exchange(code: &str) -> Option<&'static str> {
match code {
s if s.starts_with('6') => Some("SH"),
s if s.starts_with('0') || s.starts_with('3') || s.starts_with('2') => Some("SZ"),
s if s.starts_with('8') || s.starts_with('4') => Some("BJ"),
_ => None,
}
}
#[must_use]
pub fn normalize_hk_symbol(symbol: &str) -> Option<String> {
let trimmed = symbol.trim().trim_start_matches('0').to_string();
if !trimmed.is_empty() && trimmed.len() <= 5 && trimmed.chars().all(|c| c.is_ascii_digit()) {
return Some(format!("{trimmed:0>5}"));
}
if let Some((code, suffix)) = symbol.trim().split_once('.')
&& suffix.eq_ignore_ascii_case("HK")
{
let code_trimmed = code.trim_start_matches('0');
if !code_trimmed.is_empty()
&& code_trimmed.len() <= 5
&& code_trimmed.chars().all(|c| c.is_ascii_digit())
{
return Some(format!("{code_trimmed:0>5}"));
}
}
None
}
pub fn tencent_market_symbol(symbol: &str) -> crate::error::Result<String> {
let normalized = normalize_a_share_symbol(symbol).ok_or_else(|| {
crate::error::Error::invalid_input(format!("invalid A-share symbol: {symbol}"))
})?;
let (code, suffix) = normalized.split_once('.').ok_or_else(|| {
crate::error::Error::invalid_input(format!("invalid symbol format: {normalized}"))
})?;
let prefix = match suffix {
"SH" => "sh",
"SZ" | "BJ" => "sz",
_ => {
return Err(crate::error::Error::invalid_input(format!(
"unsupported suffix: {suffix}"
)));
}
};
Ok(format!("{prefix}{code}"))
}
pub fn eastmoney_secid(symbol: &str) -> crate::error::Result<String> {
let normalized = normalize_a_share_symbol(symbol).ok_or_else(|| {
crate::error::Error::invalid_input(format!("invalid A-share symbol: {symbol}"))
})?;
let (code, suffix) = normalized.split_once('.').ok_or_else(|| {
crate::error::Error::invalid_input(format!("invalid symbol format: {normalized}"))
})?;
let market = match suffix {
"SH" => "1",
"SZ" | "BJ" => "0",
_ => {
return Err(crate::error::Error::invalid_input(format!(
"unsupported suffix: {suffix}"
)));
}
};
Ok(format!("{market}.{code}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_a_share() {
assert_eq!(
normalize_a_share_symbol("600000"),
Some("600000.SH".to_string())
);
assert_eq!(
normalize_a_share_symbol("000001"),
Some("000001.SZ".to_string())
);
assert_eq!(
normalize_a_share_symbol("600000.SH"),
Some("600000.SH".to_string())
);
assert_eq!(
normalize_a_share_symbol("sh600000"),
Some("600000.SH".to_string())
);
assert_eq!(normalize_a_share_symbol("AAPL"), None);
}
#[test]
fn test_normalize_hk() {
assert_eq!(normalize_hk_symbol("00593"), Some("00593".to_string()));
assert_eq!(normalize_hk_symbol("593"), Some("00593".to_string()));
assert_eq!(normalize_hk_symbol("00593.HK"), Some("00593".to_string()));
}
#[test]
fn test_detect_market() {
assert_eq!(detect_market("600000"), MarketKind::AShare);
assert_eq!(detect_market("00593"), MarketKind::HongKong);
assert_eq!(detect_market("AAPL"), MarketKind::UsEquity);
}
}