#![warn(
unknown_lints,
// ---------- Stylistic
absolute_paths_not_starting_with_crate,
elided_lifetimes_in_paths,
explicit_outlives_requirements,
macro_use_extern_crate,
nonstandard_style, /* group */
noop_method_call,
rust_2018_idioms,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
// ---------- Future
future_incompatible, /* group */
rust_2021_compatibility, /* group */
// ---------- Public
missing_debug_implementations,
// missing_docs,
unreachable_pub,
// ---------- Unsafe
unsafe_code,
unsafe_op_in_unsafe_fn,
// ---------- Unused
unused, /* group */
)]
#![deny(
// ---------- Public
exported_private_dependencies,
private_in_public,
// ---------- Deprecated
anonymous_parameters,
bare_trait_objects,
ellipsis_inclusive_range_patterns,
// ---------- Unsafe
deref_nullptr,
drop_bounds,
dyn_drop,
)]
use codes_agency::{standardized_type, Agency, Standard};
use codes_check_digits::{luhn, Calculator};
use codes_common::error::{invalid_format, invalid_length};
use codes_common::{fixed_length_code, Code};
use codes_iso_3166::part_1::CountryCode;
use std::{fmt::Display, fmt::Formatter, str::FromStr};
use tracing::warn;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const ISO_6166: Standard = Standard::new_with_long_ref(
Agency::ISO,
"6166",
"ISO 6166-1:2021",
"Financial services — International securities identification number (ISIN)",
"https://www.iso.org/standard/78502.html",
);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct InternationalSecuritiesId {
country: CountryCode,
nsin: String,
check_digit: u8,
}
pub use codes_common::CodeParseError as InternationalSecuritiesIdError;
const TYPE_NAME: &str = "InternationalSecuritiesId";
impl Display for InternationalSecuritiesId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
if f.alternate() {
format!("{}-{:0>9}-{}", self.country, &self.nsin, &self.check_digit)
} else {
format!("{}{:0>9}{}", self.country, self.nsin, self.check_digit)
}
)
}
}
impl From<InternationalSecuritiesId> for String {
fn from(v: InternationalSecuritiesId) -> String {
v.to_string()
}
}
impl FromStr for InternationalSecuritiesId {
type Err = InternationalSecuritiesIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() == 14 {
Self::from_str(&s.replace('-', ""))
} else if s.len() != 12 {
warn!("ISIN must be 12 characters long, not {}", s.len());
Err(invalid_length(TYPE_NAME, s.len()))
} else if let Ok(country_code) = CountryCode::from_str(&s[0..2]) {
let cd_calc = luhn::get_algorithm_instance();
cd_calc.validate(s)?;
let nsin = &s[2..11];
Ok(InternationalSecuritiesId {
country: country_code,
nsin: nsin.to_string(),
check_digit: u8::from_str(&s[11..]).map_err(|_| invalid_format(TYPE_NAME, s))?,
})
} else {
warn!(
"ISIN must have a valid ISO country code as first two characters, not {:?}",
&s[0..2]
);
Err(invalid_format(TYPE_NAME, s))
}
}
}
#[cfg(feature = "urn")]
impl TryFrom<url::Url> for InternationalSecuritiesId {
type Error = InternationalSecuritiesIdError;
fn try_from(value: url::Url) -> Result<Self, Self::Error> {
if !value.scheme().eq_ignore_ascii_case("urn") {
Err(invalid_format(TYPE_NAME, value.scheme()))
} else {
let path = value.path();
if path[0..5].eq_ignore_ascii_case("isin:") {
InternationalSecuritiesId::from_str(&path[4..])
} else {
warn!("URN authority is not ISIN");
Err(invalid_format(TYPE_NAME, path))
}
}
}
}
#[cfg(feature = "urn")]
impl From<InternationalSecuritiesId> for url::Url {
fn from(id: InternationalSecuritiesId) -> url::Url {
url::Url::parse(&format!("urn:isin:{:#}", id)).unwrap()
}
}
impl Code<String> for InternationalSecuritiesId {}
fixed_length_code!(InternationalSecuritiesId, 12);
standardized_type!(InternationalSecuritiesId, ISO_6166);
impl InternationalSecuritiesId {
pub fn new(country: CountryCode, nsin: &str) -> Result<Self, InternationalSecuritiesIdError> {
let cd_calc = luhn::get_algorithm_instance();
let check_digit = cd_calc.calculate(&format!("{}{:0>9}", country, nsin))?;
Ok(Self {
country,
nsin: nsin.to_string(),
check_digit,
})
}
pub fn country_code(&self) -> CountryCode {
self.country
}
pub fn national_number(&self) -> &String {
&self.nsin
}
pub fn check_digit(&self) -> u8 {
self.check_digit
}
}
#[cfg(feature = "old_nsin")]
fn validate_nsin(country: &CountryCode, s: &str) -> Result<String, InternationalSecuritiesIdError> {
if let Some(nsin) = nsin::national_number_scheme_for(country) {
if nsin.is_valid(s) {
Ok(s.to_string())
} else {
warn!("NSID value {:?} is not a valid {}", s, nsin.name());
Err(invalid_format(nsin.name(), s))
}
} else {
Ok(s.to_string())
}
}
pub mod nsin;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_formatting() {
let isin = InternationalSecuritiesId::from_str("US9311421039").unwrap();
assert_eq!(format!("{}", isin), "US9311421039".to_string());
assert_eq!(format!("{:#}", isin), "US-931142103-9".to_string());
}
#[test]
fn test_from_str() {
let isin = InternationalSecuritiesId::from_str("US9311421039").unwrap();
assert_eq!(format!("{}", isin), "US9311421039".to_string());
let isin = InternationalSecuritiesId::from_str("US-931142103-9").unwrap();
assert_eq!(format!("{}", isin), "US9311421039".to_string());
}
#[test]
fn test_us_cusip() {
let isin = InternationalSecuritiesId::new(CountryCode::US, "37833100").unwrap();
assert_eq!(format!("{}", isin), "US0378331005".to_string());
assert_eq!(format!("{:#}", isin), "US-037833100-5".to_string());
}
#[test]
fn test_swiss_valor() {
let isin = InternationalSecuritiesId::new(CountryCode::CH, "1213853").unwrap();
assert_eq!(format!("{}", isin), "CH0012138530".to_string());
assert_eq!(format!("{:#}", isin), "CH-001213853-0".to_string());
}
#[test]
fn test_uk_sedol() {
let isin = InternationalSecuritiesId::new(CountryCode::GB, "263494").unwrap();
assert_eq!(format!("{}", isin), "GB0002634946".to_string());
assert_eq!(format!("{:#}", isin), "GB-000263494-6".to_string());
}
#[test]
fn test_australia() {
let isin = InternationalSecuritiesId::new(CountryCode::AU, "XVGZA").unwrap();
assert_eq!(format!("{}", isin), "AU0000XVGZA3".to_string());
assert_eq!(format!("{:#}", isin), "AU-0000XVGZA-3".to_string());
}
#[test]
fn test_japan() {
let isin = InternationalSecuritiesId::new(CountryCode::JP, "K0VF05").unwrap();
assert_eq!(format!("{}", isin), "JP000K0VF055".to_string());
assert_eq!(format!("{:#}", isin), "JP-000K0VF05-5".to_string());
}
}