use crate::bic::Bic;
use crate::cfi::Cfi;
use crate::cusip::Cusip;
use crate::figi::Figi;
use crate::isin::Isin;
use crate::lei::Lei;
use crate::mic::Mic;
use crate::sedol::Sedol;
use crate::valor::Valor;
use crate::wkn::Wkn;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentifierKind {
Isin,
Cusip,
Sedol,
Lei,
Figi,
Bic,
Mic,
Cfi,
Wkn,
Valor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityId {
Isin(Isin),
Cusip(Cusip),
Sedol(Sedol),
Lei(Lei),
Figi(Figi),
Bic(Bic),
Mic(Mic),
Cfi(Cfi),
Wkn(Wkn),
Valor(Valor),
}
impl SecurityId {
#[must_use]
pub fn detect(s: &str) -> Option<Self> {
if let Ok(lei) = Lei::parse(s) {
return Some(Self::Lei(lei));
}
if let Ok(isin) = Isin::parse(s) {
return Some(Self::Isin(isin));
}
if let Ok(figi) = Figi::parse(s) {
return Some(Self::Figi(figi));
}
if let Ok(cusip) = Cusip::parse(s) {
return Some(Self::Cusip(cusip));
}
if let Ok(sedol) = Sedol::parse(s) {
return Some(Self::Sedol(sedol));
}
if let Ok(bic) = Bic::parse(s) {
return Some(Self::Bic(bic));
}
#[cfg(feature = "mic-registry")]
if let Ok(mic) = Mic::parse_registered(s) {
return Some(Self::Mic(mic));
}
None
}
#[must_use]
pub fn kind(&self) -> IdentifierKind {
match self {
Self::Isin(_) => IdentifierKind::Isin,
Self::Cusip(_) => IdentifierKind::Cusip,
Self::Sedol(_) => IdentifierKind::Sedol,
Self::Lei(_) => IdentifierKind::Lei,
Self::Figi(_) => IdentifierKind::Figi,
Self::Bic(_) => IdentifierKind::Bic,
Self::Mic(_) => IdentifierKind::Mic,
Self::Cfi(_) => IdentifierKind::Cfi,
Self::Wkn(_) => IdentifierKind::Wkn,
Self::Valor(_) => IdentifierKind::Valor,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Isin(v) => v.as_str(),
Self::Cusip(v) => v.as_str(),
Self::Sedol(v) => v.as_str(),
Self::Lei(v) => v.as_str(),
Self::Figi(v) => v.as_str(),
Self::Bic(v) => v.as_str(),
Self::Mic(v) => v.as_str(),
Self::Cfi(v) => v.as_str(),
Self::Wkn(v) => v.as_str(),
Self::Valor(v) => v.as_str(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::debug;
#[test]
fn detects_lei() {
let id = SecurityId::detect("5493001KJTIIGC8Y1R12").unwrap();
assert_eq!(id.kind(), IdentifierKind::Lei);
assert_eq!(id.as_str(), "5493001KJTIIGC8Y1R12");
assert!(matches!(id, SecurityId::Lei(_)));
}
#[test]
fn detects_isin() {
let id = SecurityId::detect("US0378331005").unwrap();
assert_eq!(id.kind(), IdentifierKind::Isin);
assert_eq!(id.as_str(), "US0378331005");
assert!(matches!(id, SecurityId::Isin(_)));
}
#[test]
fn detects_figi() {
let id = SecurityId::detect("BBG000BLNNH6").unwrap();
assert_eq!(id.kind(), IdentifierKind::Figi);
assert_eq!(id.as_str(), "BBG000BLNNH6");
assert!(matches!(id, SecurityId::Figi(_)));
}
#[test]
fn detects_cusip() {
let id = SecurityId::detect("037833100").unwrap();
assert_eq!(id.kind(), IdentifierKind::Cusip);
assert_eq!(id.as_str(), "037833100");
assert!(matches!(id, SecurityId::Cusip(_)));
}
#[test]
fn detects_sedol() {
let id = SecurityId::detect("0263494").unwrap();
assert_eq!(id.kind(), IdentifierKind::Sedol);
assert_eq!(id.as_str(), "0263494");
assert!(matches!(id, SecurityId::Sedol(_)));
}
#[test]
fn detects_bic() {
let id = SecurityId::detect("DEUTDEFF").unwrap();
assert_eq!(id.kind(), IdentifierKind::Bic);
assert_eq!(id.as_str(), "DEUTDEFF");
assert!(matches!(id, SecurityId::Bic(_)));
}
#[test]
fn detects_eleven_character_bic() {
let id = SecurityId::detect("DEUTDEFF500").unwrap();
assert_eq!(id.kind(), IdentifierKind::Bic);
assert_eq!(id.as_str(), "DEUTDEFF500");
}
#[test]
fn isin_wins_over_figi_at_twelve_characters() {
let id = SecurityId::detect("US0378331005").unwrap();
assert_eq!(id.kind(), IdentifierKind::Isin);
}
#[test]
fn figi_is_not_mislabelled_as_isin() {
let id = SecurityId::detect("BBG000BLNNH6").unwrap();
assert_eq!(id.kind(), IdentifierKind::Figi);
}
#[test]
fn returns_none_for_garbage() {
for bad in [
"",
"garbage",
"not-an-identifier",
"1234",
"!!!!!!!!",
"lowercaseinput",
] {
assert!(
SecurityId::detect(bad).is_none(),
"{bad} should not be detected"
);
}
}
#[test]
fn returns_none_for_bad_check_digit() {
assert!(SecurityId::detect("US0378331004").is_none());
assert!(SecurityId::detect("037833101").is_none());
}
#[test]
fn structural_only_kinds_are_not_detected() {
assert!(Cfi::parse("ESVUFR").is_ok());
assert!(SecurityId::detect("ESVUFR").is_none());
assert!(Wkn::parse("A1EWWW").is_ok());
assert!(SecurityId::detect("A1EWWW").is_none());
assert!(Valor::parse("1213853").is_ok());
let valor_detect = SecurityId::detect("1213853");
assert!(valor_detect.is_none_or(|id| id.kind() != IdentifierKind::Valor));
}
#[test]
fn kind_for_every_variant() {
let isin = Isin::parse("US0378331005").unwrap();
let cusip = Cusip::parse("037833100").unwrap();
let sedol = Sedol::parse("0263494").unwrap();
let lei = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
let figi = Figi::parse("BBG000BLNNH6").unwrap();
let bic = Bic::parse("DEUTDEFF").unwrap();
let mic = Mic::parse("XNAS").unwrap();
let cfi = Cfi::parse("ESVUFR").unwrap();
let wkn = Wkn::parse("A1EWWW").unwrap();
let valor = Valor::parse("1213853").unwrap();
assert_eq!(SecurityId::Isin(isin).kind(), IdentifierKind::Isin);
assert_eq!(SecurityId::Cusip(cusip).kind(), IdentifierKind::Cusip);
assert_eq!(SecurityId::Sedol(sedol).kind(), IdentifierKind::Sedol);
assert_eq!(SecurityId::Lei(lei).kind(), IdentifierKind::Lei);
assert_eq!(SecurityId::Figi(figi).kind(), IdentifierKind::Figi);
assert_eq!(SecurityId::Bic(bic).kind(), IdentifierKind::Bic);
assert_eq!(SecurityId::Mic(mic).kind(), IdentifierKind::Mic);
assert_eq!(SecurityId::Cfi(cfi).kind(), IdentifierKind::Cfi);
assert_eq!(SecurityId::Wkn(wkn).kind(), IdentifierKind::Wkn);
assert_eq!(SecurityId::Valor(valor).kind(), IdentifierKind::Valor);
}
#[test]
fn as_str_for_every_variant() {
let isin = Isin::parse("US0378331005").unwrap();
let cusip = Cusip::parse("037833100").unwrap();
let sedol = Sedol::parse("0263494").unwrap();
let lei = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
let figi = Figi::parse("BBG000BLNNH6").unwrap();
let bic = Bic::parse("DEUTDEFF").unwrap();
let mic = Mic::parse("XNAS").unwrap();
let cfi = Cfi::parse("ESVUFR").unwrap();
let wkn = Wkn::parse("A1EWWW").unwrap();
let valor = Valor::parse("1213853").unwrap();
assert_eq!(SecurityId::Isin(isin).as_str(), "US0378331005");
assert_eq!(SecurityId::Cusip(cusip).as_str(), "037833100");
assert_eq!(SecurityId::Sedol(sedol).as_str(), "0263494");
assert_eq!(SecurityId::Lei(lei).as_str(), "5493001KJTIIGC8Y1R12");
assert_eq!(SecurityId::Figi(figi).as_str(), "BBG000BLNNH6");
assert_eq!(SecurityId::Bic(bic).as_str(), "DEUTDEFF");
assert_eq!(SecurityId::Mic(mic).as_str(), "XNAS");
assert_eq!(SecurityId::Cfi(cfi).as_str(), "ESVUFR");
assert_eq!(SecurityId::Wkn(wkn).as_str(), "A1EWWW");
assert_eq!(SecurityId::Valor(valor).as_str(), "1213853");
}
#[test]
fn detected_value_round_trips_through_as_str() {
for &s in &[
"5493001KJTIIGC8Y1R12",
"US0378331005",
"BBG000BLNNH6",
"037833100",
"0263494",
"DEUTDEFF",
"DEUTDEFF500",
] {
let id = SecurityId::detect(s).unwrap_or_else(|| panic!("{s} should detect"));
assert_eq!(id.as_str(), s);
}
}
#[test]
fn identifier_kind_is_copy_and_eq() {
let a = IdentifierKind::Isin;
let b = a; assert_eq!(a, b);
assert_ne!(IdentifierKind::Isin, IdentifierKind::Figi);
}
#[test]
fn security_id_is_copy_and_eq() {
let a = SecurityId::detect("US0378331005").unwrap();
let b = a; assert_eq!(a, b);
assert_ne!(a, SecurityId::detect("037833100").unwrap());
}
#[test]
fn debug_renders_kind_and_variant() {
assert!(debug(IdentifierKind::Lei).as_str().contains("Lei"));
let id = SecurityId::detect("US0378331005").unwrap();
assert!(debug(id).as_str().contains("Isin"));
}
#[cfg(feature = "mic-registry")]
#[test]
fn detects_registered_mic() {
let id = SecurityId::detect("XNAS").unwrap();
assert_eq!(id.kind(), IdentifierKind::Mic);
assert_eq!(id.as_str(), "XNAS");
assert!(matches!(id, SecurityId::Mic(_)));
}
#[cfg(feature = "mic-registry")]
#[test]
fn unregistered_mic_is_not_detected() {
assert!(Mic::parse("ZZZZ").is_ok());
assert!(SecurityId::detect("ZZZZ").is_none());
}
}