codes_iso_6166/
lib.rs

1/*!
2This package contains an implementation of the [ISO
36166](https://www.iso.org/standard/78502.html) International securities
4identification number (ISIN) specification.
5
6ISO 6166 defines the structure of an International Securities Identification
7Number (ISIN). An ISIN uniquely identifies a fungible security.
8
9Securities with which ISINs can be used are:
10
11* Equities (shares, units, depository receipts)
12* Debt instruments (bonds and debt instruments other than international,
13  international bonds and debt instruments, stripped coupons and principal,
14  treasury bills, others)
15* Entitlements (rights, warrants)
16* Derivatives (options, futures)
17* Others (commodities, currencies, indices, interest rates)
18
19ISINs consist of two alphabetic characters, which are the ISO 3166-1 alpha-2
20code for the issuing country, nine alpha-numeric characters (the National
21Securities Identifying Number, or NSIN, which identifies the security, padded
22as necessary with leading zeros), and one numerical check digit. They are thus
23always 12 characters in length. When the NSIN changes due to corporate actions
24or other reasons, the ISIN will also change. Issuance of ISINs is
25decentralized to individual national numbering agencies (NNAs). Since existing
26national numbering schemes administered by the various NNAs form the basis for
27ISINs, the methodology for assignment is not consistent across agencies
28globally.
29
30An ISIN cannot specify a particular trading location. Another identifier,
31typically a MIC (Market Identifier Code) or the three-letter exchange code,
32will have to be specified in addition to the ISIN for this. The currency of
33the trade will also be required to uniquely identify the instrument using this
34method.
35
36# Example
37
38The following demonstrates the most common method for constructing an ISIN,
39using the standard `FromStr` trait.
40
41```rust
42use codes_iso_3166::part_1::CountryCode;
43use codes_iso_6166::InternationalSecuritiesId as Isin;
44use std::str::FromStr;
45
46let walmart = Isin::from_str("US9311421039").unwrap();
47assert_eq!(walmart.country_code(), CountryCode::US);
48assert_eq!(walmart.national_number(), "931142103");
49assert_eq!(walmart.check_digit(), 9);
50```
51
52Alternatively, an ISIN can be constructed from a combination of ISO 3166
53country code and an NSIN string. This will calculate and append the ISIN check
54digit.
55
56``` rust
57use codes_iso_3166::part_1::CountryCode;
58use codes_iso_6166::InternationalSecuritiesId as Isin;
59use std::str::FromStr;
60
61let bae_systems = Isin::new(CountryCode::GB, "263494").unwrap();
62assert_eq!(&format!("{}", bae_systems), "GB0002634946");
63assert_eq!(&format!("{:#}", bae_systems), "GB-000263494-6");
64```
65
66# Features
67
68By default only the `serde` feature is enabled.
69
70* `serde` - Enables serialization of the [InternationalSecuritiesId] type.
71* `url` - Enables the conversion between ISIN and URL (URN) forms.
72
73*/
74
75#![warn(
76    unknown_lints,
77    // ---------- Stylistic
78    absolute_paths_not_starting_with_crate,
79    elided_lifetimes_in_paths,
80    explicit_outlives_requirements,
81    macro_use_extern_crate,
82    nonstandard_style, /* group */
83    noop_method_call,
84    rust_2018_idioms,
85    single_use_lifetimes,
86    trivial_casts,
87    trivial_numeric_casts,
88    // ---------- Future
89    future_incompatible, /* group */
90    rust_2021_compatibility, /* group */
91    // ---------- Public
92    missing_debug_implementations,
93    // missing_docs,
94    unreachable_pub,
95    // ---------- Unsafe
96    unsafe_code,
97    unsafe_op_in_unsafe_fn,
98    // ---------- Unused
99    unused, /* group */
100)]
101#![deny(
102    // ---------- Public
103    exported_private_dependencies,
104    private_in_public,
105    // ---------- Deprecated
106    anonymous_parameters,
107    bare_trait_objects,
108    ellipsis_inclusive_range_patterns,
109    // ---------- Unsafe
110    deref_nullptr,
111    drop_bounds,
112    dyn_drop,
113)]
114
115use codes_agency::{standardized_type, Agency, Standard};
116use codes_check_digits::{luhn, Calculator};
117use codes_common::error::{invalid_format, invalid_length};
118use codes_common::{fixed_length_code, Code};
119use codes_iso_3166::part_1::CountryCode;
120use std::{fmt::Display, fmt::Formatter, str::FromStr};
121use tracing::warn;
122
123#[cfg(feature = "serde")]
124use serde::{Deserialize, Serialize};
125
126// ------------------------------------------------------------------------------------------------
127// Public Types
128// ------------------------------------------------------------------------------------------------
129
130///
131/// An instance of the `Standard` struct defined in the
132/// [`codes_agency`](https://docs.rs/codes-agency/latest/codes_agency/)
133/// package that describes the ISO-6166 specification.
134///
135pub const ISO_6166: Standard = Standard::new_with_long_ref(
136    Agency::ISO,
137    "6166",
138    "ISO 6166-1:2021",
139    "Financial services — International securities identification number (ISIN)",
140    "https://www.iso.org/standard/78502.html",
141);
142
143///
144/// The ISO 6166 International Securities Identification
145/// Number ([ISIN](https://www.isin.org))
146///
147/// * A two-letter country code, drawn from a list (ISO 6166) prepared by the
148///   International Organization for Standardization (ISO). This code is
149///   assigned according to the location of a company's head office. A special
150///   code, 'XS' is used for international securities cleared through
151///   pan-European clearing systems like Euroclear and CEDEL. Depository receipt
152///   ISIN usage is unique in that the country code for the security is that of
153///   the receipt issuer, not that of the underlying security.
154/// * A nine-digit numeric identifier, called the National Securities
155///   Identifying Number (NSIN), and assigned by each country's or region's . If
156///   a national number is composed of less than nine digits, it is padded with
157///   leading zeros to become a NSIN. The numeric identifier has no intrinsic
158///   meaning it is essentially a serial number.
159/// * A single check-digit. The digit is calculated based upon the preceding
160///   11 characters/digits and uses a sum modulo 10 algorithm and helps ensure
161///   against counterfeit numbers.
162///
163#[derive(Clone, Debug, PartialEq, Eq, Hash)]
164#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
165pub struct InternationalSecuritiesId {
166    country: CountryCode,
167    // National Securities Identifying Number (NSIN)
168    nsin: String,
169    check_digit: u8,
170}
171
172pub use codes_common::CodeParseError as InternationalSecuritiesIdError;
173
174// ------------------------------------------------------------------------------------------------
175// Public Functions
176// ------------------------------------------------------------------------------------------------
177
178// ------------------------------------------------------------------------------------------------
179// Implementations
180// ------------------------------------------------------------------------------------------------
181
182const TYPE_NAME: &str = "InternationalSecuritiesId";
183
184impl Display for InternationalSecuritiesId {
185    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186        write!(
187            f,
188            "{}",
189            if f.alternate() {
190                format!("{}-{:0>9}-{}", self.country, &self.nsin, &self.check_digit)
191            } else {
192                format!("{}{:0>9}{}", self.country, self.nsin, self.check_digit)
193            }
194        )
195    }
196}
197
198impl From<InternationalSecuritiesId> for String {
199    fn from(v: InternationalSecuritiesId) -> String {
200        v.to_string()
201    }
202}
203
204impl FromStr for InternationalSecuritiesId {
205    type Err = InternationalSecuritiesIdError;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        if s.len() == 14 {
209            Self::from_str(&s.replace('-', ""))
210        } else if s.len() != 12 {
211            warn!("ISIN must be 12 characters long, not {}", s.len());
212            Err(invalid_length(TYPE_NAME, s.len()))
213        } else if let Ok(country_code) = CountryCode::from_str(&s[0..2]) {
214            let cd_calc = luhn::get_algorithm_instance();
215            cd_calc.validate(s)?;
216            let nsin = &s[2..11];
217            Ok(InternationalSecuritiesId {
218                country: country_code,
219                // old: nsin: validate_nsin(&country_code, nsid)?,
220                nsin: nsin.to_string(),
221                check_digit: u8::from_str(&s[11..]).map_err(|_| invalid_format(TYPE_NAME, s))?,
222            })
223        } else {
224            warn!(
225                "ISIN must have a valid ISO country code as first two characters, not {:?}",
226                &s[0..2]
227            );
228            Err(invalid_format(TYPE_NAME, s))
229        }
230    }
231}
232
233#[cfg(feature = "urn")]
234impl TryFrom<url::Url> for InternationalSecuritiesId {
235    type Error = InternationalSecuritiesIdError;
236
237    fn try_from(value: url::Url) -> Result<Self, Self::Error> {
238        if !value.scheme().eq_ignore_ascii_case("urn") {
239            Err(invalid_format(TYPE_NAME, value.scheme()))
240        } else {
241            let path = value.path();
242            if path[0..5].eq_ignore_ascii_case("isin:") {
243                InternationalSecuritiesId::from_str(&path[4..])
244            } else {
245                warn!("URN authority is not ISIN");
246                Err(invalid_format(TYPE_NAME, path))
247            }
248        }
249    }
250}
251
252#[cfg(feature = "urn")]
253impl From<InternationalSecuritiesId> for url::Url {
254    fn from(id: InternationalSecuritiesId) -> url::Url {
255        url::Url::parse(&format!("urn:isin:{:#}", id)).unwrap()
256    }
257}
258
259impl Code<String> for InternationalSecuritiesId {}
260
261fixed_length_code!(InternationalSecuritiesId, 12);
262
263standardized_type!(InternationalSecuritiesId, ISO_6166);
264
265impl InternationalSecuritiesId {
266    ///
267    /// Construct a new ISIN from country code and NSIN. This will
268    /// check the NSIN for validity if possible, and calculate the
269    /// ISIN check digit.
270    ///
271    pub fn new(country: CountryCode, nsin: &str) -> Result<Self, InternationalSecuritiesIdError> {
272        let cd_calc = luhn::get_algorithm_instance();
273        let check_digit = cd_calc.calculate(&format!("{}{:0>9}", country, nsin))?;
274        Ok(Self {
275            country,
276            nsin: nsin.to_string(),
277            check_digit,
278        })
279    }
280
281    ///
282    /// Return the country code of this ISIN.
283    ///
284    pub fn country_code(&self) -> CountryCode {
285        self.country
286    }
287
288    ///
289    /// Return the NSIN portion of this ISIN.
290    ///
291    pub fn national_number(&self) -> &String {
292        &self.nsin
293    }
294
295    ///
296    /// Return the check digit of this ISIN.
297    ///
298    pub fn check_digit(&self) -> u8 {
299        self.check_digit
300    }
301}
302
303// ------------------------------------------------------------------------------------------------
304// Private Functions
305// ------------------------------------------------------------------------------------------------
306
307///
308/// Wrapper to fetch an NSIN scheme for the provided country code
309/// and, if one exists, validate it.
310///
311#[cfg(feature = "old_nsin")]
312fn validate_nsin(country: &CountryCode, s: &str) -> Result<String, InternationalSecuritiesIdError> {
313    if let Some(nsin) = nsin::national_number_scheme_for(country) {
314        if nsin.is_valid(s) {
315            Ok(s.to_string())
316        } else {
317            warn!("NSID value {:?} is not a valid {}", s, nsin.name());
318            Err(invalid_format(nsin.name(), s))
319        }
320    } else {
321        Ok(s.to_string())
322    }
323}
324
325// ------------------------------------------------------------------------------------------------
326// Modules
327// ------------------------------------------------------------------------------------------------
328
329pub mod nsin;
330
331// ------------------------------------------------------------------------------------------------
332// Unit Tests
333// ------------------------------------------------------------------------------------------------
334
335#[cfg(test)]
336mod tests {
337    /*
338    use pretty_assertions::assert_eq;
339     */
340    use super::*;
341
342    #[test]
343    fn test_display_formatting() {
344        // Walmart
345        let isin = InternationalSecuritiesId::from_str("US9311421039").unwrap();
346        assert_eq!(format!("{}", isin), "US9311421039".to_string());
347        assert_eq!(format!("{:#}", isin), "US-931142103-9".to_string());
348    }
349
350    #[test]
351    fn test_from_str() {
352        let isin = InternationalSecuritiesId::from_str("US9311421039").unwrap();
353        assert_eq!(format!("{}", isin), "US9311421039".to_string());
354
355        let isin = InternationalSecuritiesId::from_str("US-931142103-9").unwrap();
356        assert_eq!(format!("{}", isin), "US9311421039".to_string());
357    }
358
359    #[test]
360    fn test_us_cusip() {
361        // Apple
362        let isin = InternationalSecuritiesId::new(CountryCode::US, "37833100").unwrap();
363        assert_eq!(format!("{}", isin), "US0378331005".to_string());
364        assert_eq!(format!("{:#}", isin), "US-037833100-5".to_string());
365    }
366
367    #[test]
368    fn test_swiss_valor() {
369        // Credit Suisse
370        let isin = InternationalSecuritiesId::new(CountryCode::CH, "1213853").unwrap();
371        assert_eq!(format!("{}", isin), "CH0012138530".to_string());
372        assert_eq!(format!("{:#}", isin), "CH-001213853-0".to_string());
373    }
374
375    #[test]
376    fn test_uk_sedol() {
377        // BAE Systems
378        let isin = InternationalSecuritiesId::new(CountryCode::GB, "263494").unwrap();
379        assert_eq!(format!("{}", isin), "GB0002634946".to_string());
380        assert_eq!(format!("{:#}", isin), "GB-000263494-6".to_string());
381    }
382
383    #[test]
384    fn test_australia() {
385        // Treasury Corporation
386        let isin = InternationalSecuritiesId::new(CountryCode::AU, "XVGZA").unwrap();
387        assert_eq!(format!("{}", isin), "AU0000XVGZA3".to_string());
388        assert_eq!(format!("{:#}", isin), "AU-0000XVGZA-3".to_string());
389    }
390
391    #[test]
392    fn test_japan() {
393        let isin = InternationalSecuritiesId::new(CountryCode::JP, "K0VF05").unwrap();
394        assert_eq!(format!("{}", isin), "JP000K0VF055".to_string());
395        assert_eq!(format!("{:#}", isin), "JP-000K0VF05-5".to_string());
396    }
397}