#![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::CUSIPError;
#[cfg(feature = "schemars")]
pub mod schemars;
pub fn compute_check_digit(s: &[u8]) -> u8 {
let sum = checksum_table(s);
b'0' + sum
}
fn validate_issuer_num_format(num: &[u8]) -> Result<(), CUSIPError> {
if num.len() != 6 {
panic!("Expected 6 bytes for Issuer Num, but got {}", num.len());
}
for b in num {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut id_copy: [u8; 6] = [0; 6];
id_copy.copy_from_slice(num);
return Err(CUSIPError::InvalidIssuerNum { was: id_copy });
}
}
Ok(())
}
fn validate_issue_num_format(num: &[u8]) -> Result<(), CUSIPError> {
if num.len() != 2 {
panic!("Expected 2 bytes for Issue Num, but got {}", num.len());
}
for b in num {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut id_copy: [u8; 2] = [0; 2];
id_copy.copy_from_slice(num);
return Err(CUSIPError::InvalidIssueNum { was: id_copy });
}
}
Ok(())
}
fn validate_check_digit_format(cd: u8) -> Result<(), CUSIPError> {
if !cd.is_ascii_digit() {
Err(CUSIPError::InvalidCheckDigit { was: cd })
} else {
Ok(())
}
}
#[deprecated(note = "Use CUSIP::parse instead.")]
#[inline]
pub fn parse(value: &str) -> Result<CUSIP, CUSIPError> {
CUSIP::parse(value)
}
#[deprecated(note = "Use CUSIP::parse_loose instead.")]
#[inline]
pub fn parse_loose(value: &str) -> Result<CUSIP, CUSIPError> {
CUSIP::parse_loose(value)
}
pub fn build_from_payload(payload: &str) -> Result<CUSIP, CUSIPError> {
if payload.len() != 8 {
return Err(CUSIPError::InvalidPayloadLength { was: payload.len() });
}
let b = &payload.as_bytes()[0..8];
let issuer_num = &b[0..6];
validate_issuer_num_format(issuer_num)?;
let issue_num = &b[6..8];
validate_issue_num_format(issue_num)?;
let mut bb = [0u8; 9];
bb[0..8].copy_from_slice(b);
bb[8] = compute_check_digit(b);
Ok(CUSIP(bb))
}
pub fn build_from_parts(issuer_num: &str, issue_num: &str) -> Result<CUSIP, CUSIPError> {
if issuer_num.len() != 6 {
return Err(CUSIPError::InvalidIssuerNumLength {
was: issuer_num.len(),
});
}
let issuer_num: &[u8] = &issuer_num.as_bytes()[0..6];
validate_issuer_num_format(issuer_num)?;
if issue_num.len() != 2 {
return Err(CUSIPError::InvalidIssueNumLength {
was: issue_num.len(),
});
}
let issue_num: &[u8] = &issue_num.as_bytes()[0..2];
validate_issue_num_format(issue_num)?;
let mut bb = [0u8; 9];
bb[0..6].copy_from_slice(issuer_num);
bb[6..8].copy_from_slice(issue_num);
bb[8] = compute_check_digit(&bb[0..8]);
Ok(CUSIP(bb))
}
pub fn validate(value: &str) -> bool {
if value.len() != 9 {
return false;
}
let b = value.as_bytes();
let issuer_num: &[u8] = &b[0..6];
if validate_issuer_num_format(issuer_num).is_err() {
return false;
}
let issue_num: &[u8] = &b[6..8];
if validate_issue_num_format(issue_num).is_err() {
return false;
}
let check_digit = b[8];
if validate_check_digit_format(check_digit).is_err() {
return false;
}
let payload = &b[0..8];
let computed_check_digit = compute_check_digit(payload);
let incorrect_check_digit = check_digit != computed_check_digit;
!incorrect_check_digit
}
fn is_cins(byte: u8) -> bool {
match byte {
(b'0'..=b'9') => false,
(b'A'..=b'Z') => true,
x => panic!("It should not be possible to have a non-ASCII-alphanumeric value here: {x:?}"),
}
}
fn is_cins_base(byte: u8) -> bool {
match byte {
(b'0'..=b'9') => false,
(b'A'..=b'H') => true,
b'I' => false,
(b'J'..=b'N') => true,
b'O' => false,
(b'P'..=b'Y') => true,
b'Z' => false,
x => panic!("It should not be possible to have a non-ASCII-alphanumeric value here: {x:?}"),
}
}
fn is_cins_extended(byte: u8) -> bool {
match byte {
(b'0'..=b'9') => false,
(b'A'..=b'H') => false,
b'I' => true,
(b'J'..=b'N') => false,
b'O' => true,
(b'P'..=b'Y') => false,
b'Z' => true,
x => panic!("It should not be possible to have a non-ASCII-alphanumeric value here: {x:?}"),
}
}
fn cins_country_code(byte: u8) -> Option<char> {
match byte {
(b'0'..=b'9') => None,
x @ (b'A'..=b'Z') => Some(x as char),
x => panic!("It should not be possible to have a non-ASCII-alphanumeric value here: {x:?}"),
}
}
#[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 CUSIP([u8; 9]);
impl AsRef<str> for CUSIP {
fn as_ref(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0[..]) } }
}
impl fmt::Display for CUSIP {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let temp = unsafe { from_utf8_unchecked(self.as_bytes()) }; write!(f, "{temp}")
}
}
impl fmt::Debug for CUSIP {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { from_utf8_unchecked(self.as_bytes()) }; write!(f, "CUSIP({temp})")
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CUSIP {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = CUSIP;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a CUSIP")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
CUSIP::parse(v).map_err(E::custom)
}
}
deserializer.deserialize_str(Visitor)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for CUSIP {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl FromStr for CUSIP {
type Err = CUSIPError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_loose(s)
}
}
impl CUSIP {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CUSIPError> {
if bytes.len() != 9 {
return Err(CUSIPError::InvalidCUSIPLength { was: bytes.len() });
}
let issuer_num: &[u8] = &bytes[0..6];
validate_issuer_num_format(issuer_num)?;
let issue_num: &[u8] = &bytes[6..8];
validate_issue_num_format(issue_num)?;
let cd = bytes[8];
validate_check_digit_format(cd)?;
let payload = &bytes[0..8];
let computed_check_digit = compute_check_digit(payload);
let incorrect_check_digit = cd != computed_check_digit;
if incorrect_check_digit {
return Err(CUSIPError::IncorrectCheckDigit {
was: cd,
expected: computed_check_digit,
});
}
let mut bb = [0u8; 9];
bb.copy_from_slice(bytes);
Ok(CUSIP(bb))
}
pub fn parse(value: &str) -> Result<CUSIP, CUSIPError> {
let bytes = value.as_bytes();
Self::from_bytes(bytes)
}
#[inline]
pub fn parse_loose(value: &str) -> Result<CUSIP, CUSIPError> {
let uc = value.to_ascii_uppercase();
let temp = uc.trim();
Self::parse(temp)
}
fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
pub fn as_cins(&self) -> Option<CINS> {
CINS::new(self)
}
pub fn is_cins(&self) -> bool {
is_cins(self.as_bytes()[0])
}
#[deprecated(note = "Use CUSIP::as_cins and CINS::is_cins_base.")]
pub fn is_cins_base(&self) -> bool {
is_cins_base(self.as_bytes()[0])
}
#[deprecated(note = "Use CUSIP::as_cins and CINS::is_cins_extended.")]
pub fn is_cins_extended(&self) -> bool {
is_cins_extended(self.as_bytes()[0])
}
#[deprecated(note = "Use CUSIP::as_cins and CINS::country_code.")]
pub fn cins_country_code(&self) -> Option<char> {
cins_country_code(self.as_bytes()[0])
}
pub fn issuer_num(&self) -> &str {
unsafe { from_utf8_unchecked(&self.as_bytes()[0..6]) } }
pub fn has_private_issuer(&self) -> bool {
let bs = self.as_bytes();
let case1 = bs[3] == b'9' && bs[4] == b'9';
let case2 = bs[0] == b'9'
&& bs[1] == b'9'
&& (bs[2].is_ascii_digit())
&& (bs[3].is_ascii_digit())
&& (bs[4].is_ascii_digit());
case1 || case2
}
pub fn issue_num(&self) -> &str {
unsafe { from_utf8_unchecked(&self.as_bytes()[6..8]) } }
pub fn is_private_issue(&self) -> bool {
let bs = self.as_bytes();
let nine_tens = bs[6] == b'9';
let digit_ones = bs[7].is_ascii_digit();
let letter_ones = (b'A'..=b'Y').contains(&bs[7]);
nine_tens && (digit_ones || letter_ones)
}
pub fn is_private_use(&self) -> bool {
self.has_private_issuer() || self.is_private_issue()
}
pub fn payload(&self) -> &str {
unsafe { from_utf8_unchecked(&self.as_bytes()[0..8]) } }
pub fn check_digit(&self) -> char {
self.as_bytes()[8] as char
}
pub fn validate(s: &str) -> bool {
crate::validate(s)
}
pub fn value(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[allow(clippy::upper_case_acronyms)]
pub struct CINS<'a>(&'a CUSIP);
impl fmt::Display for CINS<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl fmt::Debug for CINS<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CINS({})", self.0) }
}
impl<'a> TryFrom<&'a CUSIP> for CINS<'a> {
type Error = &'static str;
fn try_from(cusip: &'a CUSIP) -> Result<Self, Self::Error> {
CINS::new(cusip).ok_or("Not a valid CINS")
}
}
impl<'a> CINS<'a> {
pub fn new(cusip: &'a CUSIP) -> Option<Self> {
if is_cins(cusip.as_bytes()[0]) {
Some(CINS(cusip))
} else {
None
}
}
pub fn as_cusip(&self) -> &CUSIP {
self.0
}
pub fn country_code(&self) -> char {
self.0.as_bytes()[0] as char
}
pub fn is_base(&self) -> bool {
is_cins_base(self.0.as_bytes()[0])
}
pub fn is_extended(&self) -> bool {
is_cins_extended(self.0.as_bytes()[0])
}
pub fn issuer_num(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0.as_bytes()[1..6]) }
}
pub fn issue_num(&self) -> &str {
unsafe { from_utf8_unchecked(&self.0.as_bytes()[6..8]) } }
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn parse_cusip_for_bcc_strict() {
match CUSIP::parse("09739D100") {
Ok(cusip) => {
assert_eq!(cusip.to_string(), "09739D100");
assert_eq!(cusip.issuer_num(), "09739D");
assert_eq!(cusip.issue_num(), "10");
assert_eq!(cusip.check_digit(), '0');
assert!(!cusip.is_cins());
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn parse_cusip_for_bcc_loose() {
match CUSIP::parse_loose("\t09739d100 ") {
Ok(cusip) => {
assert_eq!(cusip.to_string(), "09739D100");
assert_eq!(cusip.issuer_num(), "09739D");
assert_eq!(cusip.issue_num(), "10");
assert_eq!(cusip.check_digit(), '0');
assert!(!cusip.is_cins());
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn validate_cusip_for_bcc() {
assert!(validate("09739D100"))
}
#[test]
fn validate_cusip_for_dfs() {
assert!(validate("254709108"))
}
#[test]
fn parse_cins() {
match CUSIP::parse("S08000AA9") {
Ok(cusip) => {
assert_eq!(cusip.to_string(), "S08000AA9");
assert_eq!(cusip.issuer_num(), "S08000");
assert_eq!(cusip.issue_num(), "AA");
assert_eq!(cusip.check_digit(), '9');
assert!(cusip.is_cins());
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn parse_example_from_standard() {
match CUSIP::parse("837649128") {
Ok(cusip) => {
assert_eq!(cusip.to_string(), "837649128");
assert_eq!(cusip.issuer_num(), "837649");
assert_eq!(cusip.issue_num(), "12");
assert_eq!(cusip.check_digit(), '8');
assert!(!cusip.is_cins());
}
Err(err) => panic!("Did not expect parsing to fail: {}", err),
}
}
#[test]
fn validate_example_from_standard() {
assert!(validate("837649128"))
}
#[test]
fn reject_empty_string() {
let res = CUSIP::parse("");
assert!(res.is_err());
}
#[test]
fn reject_lowercase_issuer_id_if_strict() {
match CUSIP::parse("99999zAA5") {
Err(CUSIPError::InvalidIssuerNum { was: _ }) => {} Err(err) => {
panic!(
"Expected Err(InvalidIssuerNum {{ ... }}), but got: Err({:?})",
err
)
}
Ok(cusip) => {
panic!(
"Expected Err(InvalidIssuerNum {{ ... }}), but got: Ok({:?})",
cusip
)
}
}
}
#[test]
fn reject_lowercase_issue_id_if_strict() {
match CUSIP::parse("99999Zaa5") {
Err(CUSIPError::InvalidIssueNum { was: _ }) => {} Err(err) => {
panic!(
"Expected Err(InvalidIssueNum {{ ... }}), but got: Err({:?})",
err
)
}
Ok(cusip) => {
panic!(
"Expected Err(InvalidIssueNum {{ ... }}), but got: Ok({:?})",
cusip
)
}
}
}
#[test]
fn parse_cusip_with_0_check_digit() {
CUSIP::parse("09739D100").unwrap(); }
#[test]
fn parse_cusip_with_1_check_digit() {
CUSIP::parse("00724F101").unwrap(); }
#[test]
fn parse_cusip_with_2_check_digit() {
CUSIP::parse("02376R102").unwrap(); }
#[test]
fn parse_cusip_with_3_check_digit() {
CUSIP::parse("053015103").unwrap(); }
#[test]
fn parse_cusip_with_4_check_digit() {
CUSIP::parse("457030104").unwrap(); }
#[test]
fn parse_cusip_with_5_check_digit() {
CUSIP::parse("007800105").unwrap(); }
#[test]
fn parse_cusip_with_6_check_digit() {
CUSIP::parse("98421M106").unwrap(); }
#[test]
fn parse_cusip_with_7_check_digit() {
CUSIP::parse("007903107").unwrap(); }
#[test]
fn parse_cusip_with_8_check_digit() {
CUSIP::parse("921659108").unwrap(); }
#[test]
fn parse_cusip_with_9_check_digit() {
CUSIP::parse("020772109").unwrap(); }
#[test]
fn parse_bulk() {
let cases = [
"25470F104",
"254709108",
"254709108",
"25470F104",
"25470F302",
"25470M109",
"25490H106",
"25490K273",
"25490K281",
"25490K323",
"25490K331",
"25490K596",
"25490K869",
"25525P107",
"255519100",
"256135203",
"25614T309",
"256163106",
"25659T107",
"256677105",
"256746108",
"25746U109",
"25754A201",
"257554105",
"257559203",
"257651109",
"257701201",
"257867200",
"25787G100",
"25809K105",
"25820R105",
"258278100",
"258622109",
"25960P109",
"25960R105",
"25985W105",
"260003108",
"260174107",
"260557103",
"26140E600",
"26142R104",
"26152H301",
"262037104",
"262077100",
"26210C104",
"264120106",
"264147109",
"264411505",
"26441C204",
"26443V101",
"26484T106",
"265504100",
"26614N102",
"266605104",
"26745T101",
"267475101",
"268150109",
"268158201",
"26817Q886",
"268311107",
"26856L103",
"268603107",
"26874R108",
"26884L109",
"26884U109",
"268948106",
"26922A230",
"26922A248",
"26922A289",
"26922A305",
];
for case in cases.iter() {
CUSIP::parse(case).unwrap();
assert!(
validate(case),
"Successfully parsed {:?} but got false from validate()!",
case
);
}
}
#[test]
fn static_validate_method() {
assert!(CUSIP::validate("037833100")); assert!(CUSIP::validate("09739D100")); assert!(CUSIP::validate("S08000AA9")); assert!(CUSIP::validate("837649128")); assert!(CUSIP::validate("254709108"));
assert!(!CUSIP::validate("")); assert!(!CUSIP::validate("037833101")); assert!(!CUSIP::validate("037833")); assert!(!CUSIP::validate("0378331000")); assert!(!CUSIP::validate("037833!00")); assert!(!CUSIP::validate("037833a00")); assert!(!CUSIP::validate("invalid!!"));
let test_cases = [
"037833100",
"09739D100",
"S08000AA9",
"837649128",
"254709108",
"",
"037833101",
"037833",
"0378331000",
"037833!00",
"037833a00",
"invalid!!",
];
for case in test_cases.iter() {
assert_eq!(
CUSIP::validate(case),
crate::validate(case),
"CUSIP::validate() and module validate() should return same result for {:?}",
case
);
}
}
#[test]
fn value_accessor_method() {
let cusip1 = CUSIP::parse("037833100").unwrap();
assert_eq!(cusip1.value(), "037833100");
let cusip2 = CUSIP::parse("09739D100").unwrap();
assert_eq!(cusip2.value(), "09739D100");
let cusip3 = CUSIP::parse("S08000AA9").unwrap();
assert_eq!(cusip3.value(), "S08000AA9");
let cusip4 = CUSIP::parse("837649128").unwrap();
assert_eq!(cusip4.value(), "837649128");
let test_cusips = [
"037833100",
"09739D100",
"S08000AA9",
"837649128",
"254709108",
];
for cusip_str in test_cusips.iter() {
let cusip = CUSIP::parse(cusip_str).unwrap();
assert_eq!(cusip.value(), *cusip_str);
assert_eq!(cusip.value(), cusip.to_string());
assert_eq!(cusip.value(), cusip.as_ref());
assert_eq!(cusip.value(), format!("{}", cusip));
}
}
#[test]
fn new_methods_non_breaking() {
let cusip = CUSIP::parse("037833100").unwrap();
assert_eq!(cusip.issuer_num(), "037833");
assert_eq!(cusip.issue_num(), "10");
assert_eq!(cusip.check_digit(), '0');
assert!(!cusip.is_cins());
assert!(!cusip.is_private_use());
assert_eq!(cusip.payload(), "03783310");
assert_eq!(cusip.value(), "037833100");
assert!(CUSIP::validate("037833100"));
assert_eq!(cusip.to_string(), "037833100");
assert_eq!(cusip.as_ref(), "037833100");
assert_eq!(format!("{}", cusip), "037833100");
}
proptest! {
#[test]
#[allow(unused_must_use)]
fn doesnt_crash(s in "\\PC*") {
CUSIP::parse(&s);
}
}
}