#![warn(missing_docs)]
use std::fmt;
use std::str::FromStr;
use bstr::ByteSlice;
pub mod checksum;
use checksum::checksum_table;
pub mod error;
pub use error::CUSIPError;
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(())
}
}
pub fn parse(value: &str) -> Result<CUSIP, CUSIPError> {
if value.len() != 9 {
return Err(CUSIPError::InvalidCUSIPLength { was: value.len() });
}
let b = value.as_bytes();
let issuer_num: &[u8] = &b[0..6];
validate_issuer_num_format(issuer_num)?;
let issue_num: &[u8] = &b[6..8];
validate_issue_num_format(issue_num)?;
let cd = b[8];
validate_check_digit_format(cd)?;
let payload = &b[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(b);
Ok(CUSIP(bb))
}
pub fn parse_loose(value: &str) -> Result<CUSIP, CUSIPError> {
let uc = value.to_ascii_uppercase();
let temp = uc.trim();
parse(temp)
}
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
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
#[repr(transparent)]
#[allow(clippy::upper_case_acronyms)]
pub struct CUSIP([u8; 9]);
impl fmt::Display for CUSIP {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let temp = unsafe { self.as_bytes().to_str_unchecked() }; write!(f, "{}", temp)
}
}
impl fmt::Debug for CUSIP {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { self.as_bytes().to_str_unchecked() }; write!(f, "CUSIP({})", temp)
}
}
impl FromStr for CUSIP {
type Err = CUSIPError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_loose(s)
}
}
impl CUSIP {
fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
pub fn is_cins(&self) -> bool {
match self.as_bytes()[0] {
(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
),
}
}
pub fn is_cins_base(&self) -> bool {
match self.as_bytes()[0] {
(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
),
}
}
pub fn is_cins_extended(&self) -> bool {
match self.as_bytes()[0] {
(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
),
}
}
pub fn cins_country_code(&self) -> Option<char> {
match self.as_bytes()[0] {
(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
),
}
}
pub fn issuer_num(&self) -> &str {
unsafe { self.as_bytes()[0..6].to_str_unchecked() } }
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'
&& (b'0'..=b'9').contains(&bs[2])
&& (b'0'..=b'9').contains(&bs[3])
&& (b'0'..=b'9').contains(&bs[4]);
case1 || case2
}
pub fn issue_num(&self) -> &str {
unsafe { self.as_bytes()[6..8].to_str_unchecked() } }
pub fn is_private_issue(&self) -> bool {
let bs = self.as_bytes();
let nine_tens = bs[6] == b'9';
let digit_ones = (b'0'..=b'9').contains(&bs[7]);
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 { self.as_bytes()[0..8].to_str_unchecked() } }
pub fn check_digit(&self) -> char {
self.as_bytes()[8] as char
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn parse_cusip_for_bcc_strict() {
match 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_eq!(cusip.is_cins(), false);
}
Err(err) => assert!(false, "Did not expect parsing to fail: {}", err),
}
}
#[test]
fn parse_cusip_for_bcc_loose() {
match 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_eq!(cusip.is_cins(), false);
}
Err(err) => assert!(false, "Did not expect parsing to fail: {}", err),
}
}
#[test]
fn validate_cusip_for_bcc() {
assert!(true, "{}", validate("09739D100"))
}
#[test]
fn validate_cusip_for_dfs() {
assert!(true, "{}", validate("254709108"))
}
#[test]
fn parse_cins() {
match 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_eq!(cusip.is_cins(), true);
}
Err(err) => assert!(false, "Did not expect parsing to fail: {}", err),
}
}
#[test]
fn parse_example_from_standard() {
match 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_eq!(cusip.is_cins(), false);
}
Err(err) => assert!(false, "Did not expect parsing to fail: {}", err),
}
}
#[test]
fn validate_example_from_standard() {
assert!(true, "{}", validate("837649128"))
}
#[test]
fn reject_empty_string() {
let res = parse("");
assert!(res.is_err());
}
#[test]
fn reject_lowercase_issuer_id_if_strict() {
match parse("99999zAA5") {
Err(CUSIPError::InvalidIssuerNum { was: _ }) => {} Err(err) => {
assert!(
false,
"Expected Err(InvalidIssuerNum {{ ... }}), but got: Err({:?})",
err
)
}
Ok(cusip) => {
assert!(
false,
"Expected Err(InvalidIssuerNum {{ ... }}), but got: Ok({:?})",
cusip
)
}
}
}
#[test]
fn reject_lowercase_issue_id_if_strict() {
match parse("99999Zaa5") {
Err(CUSIPError::InvalidIssueNum { was: _ }) => {} Err(err) => {
assert!(
false,
"Expected Err(InvalidIssueNum {{ ... }}), but got: Err({:?})",
err
)
}
Ok(cusip) => {
assert!(
false,
"Expected Err(InvalidIssueNum {{ ... }}), but got: Ok({:?})",
cusip
)
}
}
}
#[test]
fn parse_cusip_with_0_check_digit() {
parse("09739D100").unwrap(); }
#[test]
fn parse_cusip_with_1_check_digit() {
parse("00724F101").unwrap(); }
#[test]
fn parse_cusip_with_2_check_digit() {
parse("02376R102").unwrap(); }
#[test]
fn parse_cusip_with_3_check_digit() {
parse("053015103").unwrap(); }
#[test]
fn parse_cusip_with_4_check_digit() {
parse("457030104").unwrap(); }
#[test]
fn parse_cusip_with_5_check_digit() {
parse("007800105").unwrap(); }
#[test]
fn parse_cusip_with_6_check_digit() {
parse("98421M106").unwrap(); }
#[test]
fn parse_cusip_with_7_check_digit() {
parse("007903107").unwrap(); }
#[test]
fn parse_cusip_with_8_check_digit() {
parse("921659108").unwrap(); }
#[test]
fn parse_cusip_with_9_check_digit() {
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() {
parse(case).unwrap();
assert_eq!(
true,
validate(case),
"Successfully parsed {:?} but got false from validate()!",
case
);
}
}
proptest! {
#[test]
#[allow(unused_must_use)]
fn doesnt_crash(s in "\\PC*") {
parse(&s);
}
}
}