#![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;
#[cfg(feature = "schemars")]
pub mod schemars;
fn compute_check_digit(s: &[u8]) -> u8 {
let sum = checksum_table(s);
b'0' + sum
}
fn validate_prefix_format(prefix: &[u8]) -> Result<&[u8], Error> {
if prefix.len() != 2 {
return Err(Error::InvalidPrefixArrayLength { was: prefix.len() });
}
for b in prefix {
if !(b.is_ascii_alphabetic() && b.is_ascii_uppercase()) {
let mut prefix_copy: [u8; 2] = [0; 2];
prefix_copy.copy_from_slice(prefix);
return Err(Error::InvalidPrefix { was: prefix_copy });
}
}
Ok(prefix)
}
fn validate_basic_code_format(basic_code: &[u8]) -> Result<&[u8], Error> {
if basic_code.len() != 9 {
return Err(Error::InvalidBasicCodeArrayLength {
was: basic_code.len(),
});
}
for b in basic_code {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut basic_code_copy: [u8; 9] = [0; 9];
basic_code_copy.copy_from_slice(basic_code);
return Err(Error::InvalidBasicCode {
was: basic_code_copy,
});
}
}
Ok(basic_code)
}
fn validate_check_digit_value(payload: &[u8], check_digit: u8) -> Result<u8, Error> {
if !check_digit.is_ascii_digit() {
Err(Error::InvalidCheckDigit { was: check_digit })
} else {
let computed_check_digit = compute_check_digit(payload);
if check_digit != computed_check_digit {
Err(Error::IncorrectCheckDigit {
was: check_digit,
expected: computed_check_digit,
})
} else {
Ok(check_digit)
}
}
}
pub fn parse(value: &str) -> Result<ISIN, Error> {
let value = validate(value)?;
let mut bb = [0u8; 12];
bb.copy_from_slice(value);
Ok(ISIN(bb))
}
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> {
let b = payload.as_bytes();
validate_payload_format(b)?;
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::InvalidPrefixStringLength { was: prefix.len() });
}
let prefix: &[u8] = &prefix.as_bytes()[0..2];
validate_prefix_format(prefix)?;
if basic_code.len() != 9 {
return Err(Error::InvalidBasicCodeStringLength {
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))
}
fn validate_payload_format(payload: &[u8]) -> Result<&[u8], Error> {
if payload.len() != 11 {
return Err(Error::InvalidPayloadArrayLength { was: payload.len() });
}
let prefix: &[u8] = &payload[0..2];
validate_prefix_format(prefix)?;
let basic_code: &[u8] = &payload[2..11];
validate_basic_code_format(basic_code)?;
Ok(payload)
}
pub fn validate(value: &str) -> Result<&[u8], Error> {
if value.len() != 12 {
return Err(Error::InvalidValueStringLength { was: value.len() });
}
let b = value.as_bytes();
if value.len() != 12 {
return Err(Error::InvalidValueArrayLength { was: value.len() });
}
let payload: &[u8] = &b[0..11];
validate_payload_format(payload)?;
let check_digit = b[11];
validate_check_digit_value(payload, check_digit)?;
Ok(b)
}
#[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[..]
}
pub fn prefix(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[0..2]) } }
pub fn basic_code(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[2..11]) } }
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").is_ok()); assert!(validate("JP3788600009").is_ok()); assert!(validate("DE000A0GNPZ3").is_ok()); }
#[test]
fn validate_examples_from_standard_annex_e() {
assert!(validate("JP3788600009").is_ok()); assert!(validate("US9047847093").is_ok()); assert!(validate("IE00BFXC1P95").is_ok()); assert!(validate("DE000A0GNPZ3").is_ok()); assert!(validate("XS2021448886").is_ok()); assert!(validate("US36962GXZ26").is_ok()); assert!(validate("FR0000571077").is_ok()); assert!(validate("US277847UB38").is_ok()); assert!(validate("US65412AEW80").is_ok()); assert!(validate("GB00BF0FCW58").is_ok()); assert!(validate("FR0000312928").is_ok()); assert!(validate("DE000DL3T7M1").is_ok());
assert!(validate("ES0A02234250").is_ok()); assert!(validate("EZR9HY1361L7").is_ok()); assert!(validate("CH0107166065").is_ok()); assert!(validate("XS0313614355").is_ok()); assert!(validate("DE000A0AE077").is_ok()); assert!(validate("CH0002813860").is_ok()); assert!(validate("TRLTCMB00045").is_ok()); assert!(validate("ES0SI0000005").is_ok()); assert!(validate("GB00B56Z6W79").is_ok()); assert!(validate("AU000000SKI7").is_ok()); assert!(validate("EU000A1RRN98").is_ok()); assert!(validate("LI0024807526").is_ok()); }
#[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(_)) => {}
}
}
}
}
}