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}