cik/
lib.rs

1//! # cik
2//!
3//! `cik` provides a `CIK` type for working with validated (syntax only)
4//! [Central Index Keys (CIKs)](https://www.sec.gov/page/edgar-glossary#cik) as defined in the
5//! [U.S. Securities and Exchange Commission's (SEC)](https://www.sec.gov)
6//! [Electronic Data Gathering, Analysis, and Retrieval system (EDGAR)](https://www.sec.gov/edgar.shtml)
7//! documentation.
8//!
9//! A CIK is a number of up to 10 digits length. They are sometimes rendered as strings with or without leading zeros, and
10//! sometimes are represented as integers of a sufficient number of bits to represent a 10-digit number (typically 64 bits
11//! because the maximum CIK value is 9,999,999,999 and the maximum value of a signed 32-bit integer is only 2,147,483,64;
12//! and the maximum value of an unsigned 32-bit integer is still to low at 4,294,967,295).
13//!
14//! As of 2022-05-29 the "Company Facts" data set's minimum CIK value is 1,750 and the maximum is 1,923,807. See
15//! the "Bulk data" section on the SEC's
16//! [EDGAR Application Programming Interfaces](https://www.sec.gov/edgar/sec-api-documentation) page for more information on
17//! this data set. Such values fit comfortably in 7 decimal digits or 32 bits (signed or unsigned), so you might encounter
18//! CIKs stored in values of less than 64 bits.
19//!
20//! Nonetheless, this library uses 64-bit values to ensure full conformance with the CIK definition of up to 10 decimal
21//! digits.
22//!
23//! ## Optional features
24//!
25//! This crate provides optional features for extended functionality:
26//!
27//! * `serde` - Enables serialization and deserialization via [serde](https://crates.io/crates/serde).
28//!   CIK values serialize as integers and can deserialize from integers or strings.
29//! * `schemars` - Enables JSON Schema generation via [schemars](https://crates.io/crates/schemars).
30//!   Generates schemas that accept both integer and string representations.
31//!
32//! ## Related crates
33//!
34//! This crate is part of the Financial Identifiers series:
35//!
36//! * [CIK](https://crates.io/crates/cik): Central Index Key (SEC EDGAR)
37//! * [CUSIP](https://crates.io/crates/cusip): Committee on Uniform Security Identification Procedures (ANSI X9.6-2020)
38//! * [ISIN](https://crates.io/crates/isin): International Securities Identification Number (ISO 6166:2021)
39//! * [LEI](https://crates.io/crates/lei): Legal Entity Identifier (ISO 17442:2020)
40//!
41
42use std::fmt;
43use std::str::FromStr;
44
45pub mod error;
46pub use error::CIKError;
47
48/// Parse a string to a valid CIK or an error message, requiring the string to already be only
49/// digits with no leading or trailing whitespace in addition to being the
50/// right length and format.
51pub fn parse(value: &str) -> Result<CIK, CIKError> {
52    let s: String = value.into();
53
54    if s.is_empty() || s.len() > 10 {
55        Err(CIKError::InvalidLength { was: s.len() })
56    } else {
57        match s.parse::<u64>() {
58            Ok(value) => build(value),
59            Err(_err) => Err(CIKError::InvalidFormat { was: s }),
60        }
61    }
62}
63
64/// Build a CIK from an integer _Value_.
65pub fn build(value: u64) -> Result<CIK, CIKError> {
66    if !(1..=9_999_999_999).contains(&value) {
67        return Err(CIKError::InvalidValue { was: value });
68    }
69
70    Ok(CIK(value))
71}
72
73/// Test whether or not the passed string is in valid CIK format, without producing a CIK struct
74/// value.
75pub fn validate(value: &str) -> bool {
76    if value.is_empty() || value.len() > 10 {
77        println!("Bad length: {value:?}");
78        return false;
79    }
80
81    // We make the preliminary assumption that the string is pure ASCII, so we work with the
82    // underlying bytes. If there is Unicode in the string, the bytes will be outside the
83    // allowed range and format validation will fail.
84
85    let b = value.as_bytes();
86
87    b.iter().all(|b| *b >= b'0' && *b <= b'9')
88}
89
90#[doc = include_str!("../README.md")]
91#[cfg(doctest)]
92pub struct ReadmeDoctests;
93
94/// A CIK in confirmed valid format.
95///
96/// You cannot construct a CIK value manually. This does not compile:
97///
98/// ```compile_fail
99/// use cik;
100/// let cannot_construct = cik::CIK(0);
101/// ```
102#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)]
103#[repr(transparent)]
104#[allow(clippy::upper_case_acronyms)]
105pub struct CIK(u64);
106
107impl fmt::Display for CIK {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", self.0)
110    }
111}
112
113impl fmt::Debug for CIK {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "CIK{}", self.0)
116    }
117}
118
119impl FromStr for CIK {
120    type Err = CIKError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        parse(s)
124    }
125}
126
127impl TryFrom<u64> for CIK {
128    type Error = CIKError;
129
130    fn try_from(value: u64) -> Result<Self, Self::Error> {
131        build(value)
132    }
133}
134
135impl CIK {
136    /// Return the underlying integer _Value_ of the CIK.
137    pub fn value(&self) -> u64 {
138        self.0
139    }
140}
141
142#[cfg(feature = "serde")]
143pub mod serde;
144
145#[cfg(feature = "schemars")]
146pub mod schemars;
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use proptest::prelude::*;
152
153    #[test]
154    fn parse_cik_for_apple() {
155        match parse("320193") {
156            Ok(cik) => {
157                assert_eq!(cik.to_string(), "320193");
158                assert_eq!(cik.value(), 320193);
159            }
160            Err(err) => panic!("Did not expect parsing to fail: {}", err),
161        }
162    }
163    #[test]
164    fn build_cik_for_apple() {
165        match build(320193) {
166            Ok(cik) => {
167                assert_eq!(cik.to_string(), "320193");
168                assert_eq!(cik.value(), 320193);
169            }
170            Err(err) => panic!("Did not expect building to fail: {}", err),
171        }
172    }
173
174    #[test]
175    fn reject_empty_string() {
176        let res = parse("");
177        assert!(res.is_err());
178    }
179
180    #[test]
181    fn reject_non_digit_string() {
182        let res = parse("-1");
183        assert!(res.is_err());
184    }
185
186    #[test]
187    fn reject_zero_string() {
188        let res = parse("0");
189        assert!(res.is_err());
190    }
191
192    #[test]
193    fn reject_long_string() {
194        let res = parse("10000000000");
195        assert!(res.is_err());
196    }
197
198    #[test]
199    fn reject_zero_value() {
200        let res = build(0);
201        assert!(res.is_err());
202    }
203
204    #[test]
205    fn reject_large_value() {
206        let res = build(10_000_000_000);
207        assert!(res.is_err());
208    }
209
210    proptest! {
211        #[test]
212        #[allow(unused_must_use)]
213        fn doesnt_crash(s in "\\PC*") {
214            parse(&s);
215        }
216    }
217}