#![warn(missing_docs)]
use std::fmt;
use std::str::from_utf8_unchecked;
use std::str::FromStr;
pub mod checksum;
use checksum::checksum_table;
pub mod error;
pub use error::Error;
#[deprecated(since = "0.1.8", note = "please use `Error` instead")]
pub type ParseError = Error;
#[deprecated(since = "0.1.18", note = "please use `Error` instead")]
pub type ISINError = Error;
pub fn compute_check_digit(s: &[u8]) -> u8 {
let sum = checksum_table(s);
b'0' + sum
}
fn validate_prefix_format(cc: &[u8]) -> Result<(), Error> {
for b in cc {
if !(b.is_ascii_alphabetic() && b.is_ascii_uppercase()) {
let mut cc_copy: [u8; 2] = [0; 2];
cc_copy.copy_from_slice(cc);
return Err(Error::InvalidPrefix { was: cc_copy });
}
}
Ok(())
}
fn validate_basic_code_format(si: &[u8]) -> Result<(), Error> {
for b in si {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut si_copy: [u8; 9] = [0; 9];
si_copy.copy_from_slice(si);
return Err(Error::InvalidBasicCode { was: si_copy });
}
}
Ok(())
}
fn validate_check_digit_format(cd: u8) -> Result<(), Error> {
if !cd.is_ascii_digit() {
Err(Error::InvalidCheckDigit { was: cd })
} else {
Ok(())
}
}
pub fn parse(value: &str) -> Result<ISIN, Error> {
if value.len() != 12 {
return Err(Error::InvalidLength { was: value.len() });
}
let b = value.as_bytes();
let cc: &[u8] = &b[0..2];
validate_prefix_format(cc)?;
let si: &[u8] = &b[2..11];
validate_basic_code_format(si)?;
let cd = b[11];
validate_check_digit_format(cd)?;
let payload = &b[0..11];
let computed_check_digit = compute_check_digit(payload);
let incorrect_check_digit = cd != computed_check_digit;
if incorrect_check_digit {
return Err(Error::IncorrectCheckDigit {
was: cd,
expected: computed_check_digit,
});
}
let mut bb = [0u8; 12];
bb.copy_from_slice(b);
Ok(ISIN(bb))
}
#[deprecated(since = "0.1.7", note = "please use `isin::parse` instead")]
pub fn parse_strict(value: &str) -> Result<ISIN, Error> {
parse(value)
}
pub fn parse_loose(value: &str) -> Result<ISIN, Error> {
let uc = value.to_ascii_uppercase();
let temp = uc.trim();
parse(temp)
}
pub fn build_from_payload(payload: &str) -> Result<ISIN, Error> {
if payload.len() != 11 {
return Err(Error::InvalidPayloadLength { was: payload.len() });
}
let b = &payload.as_bytes()[0..11];
let prefix = &b[0..2];
validate_prefix_format(prefix)?;
let basic_code = &b[2..11];
validate_basic_code_format(basic_code)?;
let mut bb = [0u8; 12];
bb[0..11].copy_from_slice(b);
bb[11] = compute_check_digit(b);
Ok(ISIN(bb))
}
pub fn build_from_parts(prefix: &str, basic_code: &str) -> Result<ISIN, Error> {
if prefix.len() != 2 {
return Err(Error::InvalidPrefixLength { was: prefix.len() });
}
let prefix: &[u8] = &prefix.as_bytes()[0..2];
validate_prefix_format(prefix)?;
if basic_code.len() != 9 {
return Err(Error::InvalidBasicCodeLength {
was: basic_code.len(),
});
}
let basic_code: &[u8] = &basic_code.as_bytes()[0..9];
validate_basic_code_format(basic_code)?;
let mut bb = [0u8; 12];
bb[0..2].copy_from_slice(prefix);
bb[2..11].copy_from_slice(basic_code);
bb[11] = compute_check_digit(&bb[0..11]);
Ok(ISIN(bb))
}
pub fn validate(value: &str) -> bool {
if value.len() != 12 {
return false;
}
let b = value.as_bytes();
let prefix: &[u8] = &b[0..2];
if validate_prefix_format(prefix).is_err() {
return false;
}
let basic_code: &[u8] = &b[2..11];
if validate_basic_code_format(basic_code).is_err() {
return false;
}
let cd = b[11];
if validate_check_digit_format(cd).is_err() {
return false;
}
let payload = &b[0..11];
let computed_check_digit = compute_check_digit(payload);
let incorrect_check_digit = cd != computed_check_digit;
!incorrect_check_digit
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)]
#[repr(transparent)]
#[allow(clippy::upper_case_acronyms)]
pub struct ISIN([u8; 12]);
impl AsRef<str> for ISIN {
fn as_ref(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[..]) } }
}
impl fmt::Display for ISIN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { from_utf8_unchecked(self.as_bytes()) }; write!(f, "{temp}")
}
}
impl fmt::Debug for ISIN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { from_utf8_unchecked(self.as_bytes()) }; write!(f, "ISIN({temp})")
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ISIN {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = ISIN;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an ISIN")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
crate::parse(v).map_err(E::custom)
}
}
deserializer.deserialize_str(Visitor)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ISIN {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl FromStr for ISIN {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_loose(s)
}
}
impl ISIN {
fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
#[deprecated(since = "0.1.7", note = "please use `isin::parse` instead")]
pub fn parse_strict<S>(value: S) -> Result<ISIN, Error>
where
S: Into<String>,
{
let v: String = value.into();
crate::parse(&v)
}
#[deprecated(since = "0.1.7", note = "please use `isin::parse_loose` instead")]
pub fn parse_loose<S>(value: S) -> Result<ISIN, Error>
where
S: Into<String>,
{
let v: String = value.into();
crate::parse_loose(&v)
}
#[deprecated(since = "0.1.7", note = "please use `to_string` instead")]
pub fn value(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[..]) } }
pub fn prefix(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[0..2]) } }
#[deprecated(since = "0.1.8", note = "please use `prefix` instead")]
pub fn country_code(&self) -> &str {
self.prefix()
}
pub fn basic_code(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[2..11]) } }
#[deprecated(since = "0.1.8", note = "please use `basic_code` instead")]
pub fn security_identifier(&self) -> &str {
self.basic_code()
}
pub fn payload(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[0..11]) } }
pub fn check_digit(&self) -> char {
self.0[11] as char
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn parse_isin_for_apple_strict() {
match parse("US0378331005") {
Ok(isin) => {
assert_eq!(isin.to_string(), "US0378331005");
assert_eq!(isin.prefix(), "US");
assert_eq!(isin.basic_code(), "037833100");
assert_eq!(isin.check_digit(), '5');
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn build_isin_for_apple_from_payload() {
match build_from_payload("US037833100") {
Ok(isin) => {
assert_eq!(isin.to_string(), "US0378331005");
assert_eq!(isin.prefix(), "US");
assert_eq!(isin.basic_code(), "037833100");
assert_eq!(isin.check_digit(), '5');
}
Err(err) => panic!("Did not expect building to fail: {}", err),
}
}
#[test]
fn build_isin_for_apple_from_parts() {
match build_from_parts("US", "037833100") {
Ok(isin) => {
assert_eq!(isin.to_string(), "US0378331005");
assert_eq!(isin.prefix(), "US");
assert_eq!(isin.basic_code(), "037833100");
assert_eq!(isin.check_digit(), '5');
}
Err(err) => panic!("Did not expect building to fail: {}", err),
}
}
#[test]
fn parse_isin_for_apple_loose() {
match parse_loose("\tus0378331005 ") {
Ok(isin) => {
assert_eq!(isin.to_string(), "US0378331005");
assert_eq!(isin.prefix(), "US");
assert_eq!(isin.basic_code(), "037833100");
assert_eq!(isin.check_digit(), '5');
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn validate_examples_from_standard_annex_c() {
assert!(validate("ES0SI0000005")); assert!(validate("JP3788600009")); assert!(validate("DE000A0GNPZ3")); }
#[test]
fn validate_examples_from_standard_annex_e() {
assert!(validate("JP3788600009")); assert!(validate("US9047847093")); assert!(validate("IE00BFXC1P95")); assert!(validate("DE000A0GNPZ3")); assert!(validate("XS2021448886")); assert!(validate("US36962GXZ26")); assert!(validate("FR0000571077")); assert!(validate("US277847UB38")); assert!(validate("US65412AEW80")); assert!(validate("GB00BF0FCW58")); assert!(validate("FR0000312928")); assert!(validate("DE000DL3T7M1")); assert!(validate("ES0A02234250")); assert!(validate("EZR9HY1361L7")); assert!(validate("CH0107166065")); assert!(validate("XS0313614355")); assert!(validate("DE000A0AE077")); assert!(validate("CH0002813860")); assert!(validate("TRLTCMB00045")); assert!(validate("ES0SI0000005")); assert!(validate("GB00B56Z6W79")); assert!(validate("AU000000SKI7")); assert!(validate("EU000A1RRN98")); assert!(validate("LI0024807526")); }
#[test]
fn reject_empty_string() {
let res = parse("");
assert!(res.is_err());
}
#[test]
fn reject_lowercase_prefix_if_strict() {
match parse("us0378331005") {
Err(Error::InvalidPrefix { was: _ }) => {} Err(err) => {
panic!(
"Expected Err(InvalidPrefix {{ ... }}), but got: Err({:?})",
err
)
}
Ok(isin) => {
panic!(
"Expected Err(InvalidPrefix {{ ... }}), but got: Ok({:?})",
isin
)
}
}
}
#[test]
fn reject_lowercase_basic_code_if_strict() {
match parse("US09739d1000") {
Err(Error::InvalidBasicCode { was: _ }) => {} Err(err) => {
panic!(
"Expected Err(InvalidBasicCode {{ ... }}), but got: Err({:?})",
err
)
}
Ok(isin) => {
panic!(
"Expected Err(InvalidBasicCode {{ ... }}), but got: Ok({:?})",
isin
)
}
}
}
#[test]
fn parse_isin_with_0_check_digit() {
parse("US09739D1000").unwrap(); }
#[test]
fn parse_isin_with_1_check_digit() {
parse("US4581401001").unwrap(); }
#[test]
fn parse_isin_with_2_check_digit() {
parse("US98421M1062").unwrap(); }
#[test]
fn parse_isin_with_3_check_digit() {
parse("US02376R1023").unwrap(); }
#[test]
fn parse_isin_with_4_check_digit() {
parse("US9216591084").unwrap(); }
#[test]
fn parse_isin_with_5_check_digit() {
parse("US0207721095").unwrap(); }
#[test]
fn parse_isin_with_6_check_digit() {
parse("US71363P1066").unwrap(); }
#[test]
fn parse_isin_with_7_check_digit() {
parse("US5915202007").unwrap(); }
#[test]
fn parse_isin_with_8_check_digit() {
parse("US4570301048").unwrap(); }
#[test]
fn parse_isin_with_9_check_digit() {
parse("US8684591089").unwrap(); }
#[test]
fn test_unicode_gibberish() {
assert!(parse("𑴈𐎟 0 A").is_err());
}
proptest! {
#[test]
#[allow(unused_must_use)]
fn doesnt_crash(s in "\\PC*") {
parse(&s);
}
}
#[cfg(feature = "serde")]
mod serde {
use crate::ISIN;
use proptest::{prop_assert, prop_assert_eq, proptest};
use serde::de::value::{self, StrDeserializer};
use serde::Deserialize as _;
#[test]
fn deserialize_apple() {
let isin = ISIN::deserialize(StrDeserializer::<value::Error>::new("US0378331005"))
.expect("successful deserialization");
assert_eq!(isin.to_string(), "US0378331005");
assert_eq!(isin.prefix(), "US");
assert_eq!(isin.basic_code(), "037833100");
assert_eq!(isin.check_digit(), '5');
}
#[test]
fn reject_empty_string() {
let _ = ISIN::deserialize(StrDeserializer::<value::Error>::new(""))
.expect_err("unsuccessful deserialization");
}
#[test]
fn reject_lowercase_prefix_if_strict() {
let _ = ISIN::deserialize(StrDeserializer::<value::Error>::new("us0378331005"))
.expect_err("unsuccessful deserialization");
}
#[test]
fn reject_lowercase_basic_code_if_strict() {
let _ = ISIN::deserialize(StrDeserializer::<value::Error>::new("US09739d1000"))
.expect_err("unsuccessful deserialization");
}
proptest! {
#[test]
fn doesnt_crash(s in "\\PC*") {
let _ = ISIN::deserialize(StrDeserializer::<value::Error>::new(&s));
}
#[test]
fn matches_parse(s in "\\PC*") {
let parse_result = crate::parse(&s);
let deserialize_result = ISIN::deserialize(StrDeserializer::<value::Error>::new(&s));
match (parse_result, deserialize_result)
{
(Ok(parsed_isin), Ok(deserialized_isin)) => prop_assert_eq!(parsed_isin, deserialized_isin),
(Ok(_), Err(_)) | (Err(_), Ok(_)) => prop_assert!(false),
(Err(_), Err(_)) => {}
}
}
}
}
}