Security identifiers
ISIN, CUSIP and CINS, SEDOL and FIGI checked by their check digits and told apart, ISINs made from a CUSIP, SEDOL or other national number, and ISINs found in text.
Version 1.0.0 · 4 functions · 1 crate
Functions
What use securities_ids.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use security_id_check, security_id_valid from securities_ids (Quillo 0.6).
security_id_check(id: str, kind: str) → anyChecks a security identifier and says what it is: { valid, type, id, ..., error }, type being "ISIN" (12 characters: with country and nsin, the national number inside), "CUSIP" or "CINS" (9; CINS with its region), "SEDOL" (7) or "FIGI" (12, starting BBG or another two-letter prefix and G). kind forces one of them ("" to guess from the shape).
security_id_valid(id: str) → boolWhether id is a valid ISIN, CUSIP, SEDOL or FIGI.
isin_from(nsin: str, country: str) → stror an errorAn ISIN from a national number and its country (ISO code): a CUSIP for "US" or "CA", a SEDOL for "GB" or "IE" (padded with 00), any other national number padded with zeros to nine characters (a WKN for "DE", a Valoren for "CH"). The national number's own check digit is checked first where it has one.
isin_find(text: str) → listEvery valid ISIN in a text (a fund factsheet, a broker statement), each once in the order found.
Get it
$ quillo install securities_ids # securities_ids.qwer into this folder$ quillo install securities_ids --example # and its example, securities_ids.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/securities_ids.qwer$ curl -O https://quillolang.org/plugins/files/securities_ids.qweThen, in your program, all of it or only what you call:
use securities_ids.qweruse security_id_check, security_id_valid from securities_ids // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// A portfolio import for a wealth app: each holding's identifier checked and recognized, ISINs// made for holdings known only by CUSIP or SEDOL, and ISINs pulled out of a broker's statement.//// quillo run securities_ids.qwe then open http://localhost:8080/holdings and http://localhost:8080/id/US0378331005use "securities_ids.qwer"route GET /id/:id = security_id_check(id, "")let HOLDINGS = ["US0378331005", "037833100", "0263494", "BBG000BLNNH6", "US0378331006", "DE0007164600"]route GET /holdings = { checked: HOLDINGS | map((h) => { let r = security_id_check(h, ""); r.valid ? `${h}: ${r.type}` : `${h}: ${r.error}` }), apple_from_cusip: isin_from("037833100", "US")?, bae_from_sedol: isin_from("0263494", "GB")?, sap_from_wkn: isin_from("716460", "DE")?, statement: isin_find("Bought 10 Apple Inc (US0378331005) and 5 SAP SE, ISIN DE0007164600; sold IE00B4L5Y983 iShares Core MSCI World."),}$ quillo run securities_ids.qwe$ curl 'localhost:8080/id/US0378331005'$ curl 'localhost:8080/holdings'Source
//! Security identifiers checked and converted: ISIN (any country), CUSIP and CINS, SEDOL and FIGI by their check digits, the kind told from the code itself, ISINs made from a CUSIP, SEDOL or any national number, and ISINs found in text.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};fn clean(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace() && *c != '-').map(|c| c.to_ascii_uppercase()).collect()}fn val(c: char) -> Option<u32> { match c { '0'..='9' => c.to_digit(10), 'A'..='Z' => Some(c as u32 - 'A' as u32 + 10), '*' => Some(36), '@' => Some(37), '#' => Some(38), _ => None, }}fn digit_sum(n: u32) -> u32 { n.to_string().chars().map(|c| c.to_digit(10).unwrap()).sum()}fn cusip_digit(s: &str) -> Option<u32> { let mut t = 0; for (i, c) in s.chars().take(8).enumerate() { let v = val(c)? * if i % 2 == 1 { 2 } else { 1 }; t += v / 10 + v % 10; } Some((10 - t % 10) % 10)}fn isin_digit(s: &str) -> Option<u32> { let digits: String = s.chars().take(11).map(|c| val(c).map(|v| v.to_string())).collect::<Option<String>>()?; let t: u32 = digits.chars().rev().enumerate().map(|(i, c)| { let v = c.to_digit(10).unwrap() * if i % 2 == 0 { 2 } else { 1 }; v / 10 + v % 10 }).sum(); Some((10 - t % 10) % 10)}fn sedol_digit(s: &str) -> Option<u32> { let w = [1, 3, 1, 7, 3, 9]; let t: u32 = s.chars().take(6).zip(w).map(|(c, x)| val(c).map(|v| v * x)).sum::<Option<u32>>()?; Some((10 - t % 10) % 10)}fn figi_digit(s: &str) -> Option<u32> { let mut t = 0; for (i, c) in s.chars().take(11).enumerate() { t += digit_sum(val(c)? * if i % 2 == 1 { 2 } else { 1 }); } Some((10 - t % 10) % 10)}fn last_digit(s: &str) -> Option<u32> { s.chars().last().and_then(|c| c.to_digit(10))}const CINS: &[(char, &str)] = &[('A', "Austria"), ('B', "Belgium"), ('C', "Canada"), ('D', "Germany"), ('E', "Spain"), ('F', "France"), ('G', "United Kingdom"), ('H', "Switzerland"), ('J', "Japan"), ('K', "Denmark"), ('L', "Luxembourg"), ('M', "Middle East"), ('N', "Netherlands"), ('P', "South America"), ('Q', "Australia"), ('R', "Norway"), ('S', "South Africa"), ('T', "Italy"), ('U', "United States"), ('V', "Africa (other)"), ('W', "Sweden"), ('X', "Europe (other)"), ('Y', "Asia")];fn ok(kind: &str, id: &str, extra: Value) -> Value { let mut v = json!({ "valid": true, "type": kind, "id": id }); if let Value::Object(m) = extra { v.as_object_mut().unwrap().extend(m); } v["error"] = Value::Null; v}fn bad(kind: &str, id: &str, e: &str) -> Value { json!({ "valid": false, "type": kind, "id": id, "error": e })}/// Checks a security identifier and says what it is: `{ valid, type, id, ..., error }`, `type` being "ISIN" (12 characters: with/// `country` and `nsin`, the national number inside), "CUSIP" or "CINS" (9; CINS with its `region`), "SEDOL" (7) or "FIGI" (12,/// starting BBG or another two-letter prefix and G). `kind` forces one of them ("" to guess from the shape).pub fn security_id_check(id: &str, kind: &str) -> Value { let s = clean(id); let k = if kind.is_empty() { match s.len() { // FIGI prefixes leave out the ones that would read as ISINs with a G (Bermuda's BMG..., say). 12 if s.as_bytes()[2] == b'G' && s[..2].chars().all(|c| c.is_ascii_alphabetic()) && !["BS", "BM", "GG", "GB", "GH", "KY", "VG"].contains(&&s[..2]) => "FIGI", 12 => "ISIN", 9 => "CUSIP", 7 => "SEDOL", _ => return bad("unknown", &s, "not the length of an ISIN (12), CUSIP (9), SEDOL (7) or FIGI (12)"), } } else { match kind.to_uppercase().as_str() { "ISIN" => "ISIN", "CUSIP" | "CINS" => "CUSIP", "SEDOL" => "SEDOL", "FIGI" => "FIGI", other => return bad(other, &s, "kind is ISIN, CUSIP, SEDOL or FIGI"), } }; let typo = "the check digit does not match: a typo"; match k { "ISIN" => { if s.len() != 12 || !s[..2].chars().all(|c| c.is_ascii_alphabetic()) || !s[2..11].chars().all(|c| c.is_ascii_alphanumeric()) { return bad("ISIN", &s, "an ISIN is a 2-letter country code, 9 letters or digits and a check digit"); } if isin_digit(&s) != last_digit(&s) { return bad("ISIN", &s, typo); } let nsin = &s[2..11]; let mut extra = json!({ "country": &s[..2], "nsin": nsin, "check_digit": &s[11..] }); if (s.starts_with("US") || s.starts_with("CA")) && cusip_digit(nsin) == last_digit(nsin) { extra["cusip"] = json!(nsin); } if (s.starts_with("GB") || s.starts_with("IE")) && s[2..4] == *"00" && sedol_digit(&s[4..11]) == last_digit(&s[4..11]) { extra["sedol"] = json!(&s[4..11]); } ok("ISIN", &s, extra) } "CUSIP" => { if s.len() != 9 || !s.chars().all(|c| val(c).is_some()) { return bad("CUSIP", &s, "a CUSIP is 8 letters, digits, *, @ or # and a check digit"); } if cusip_digit(&s) != last_digit(&s) { return bad("CUSIP", &s, typo); } let first = s.chars().next().unwrap(); match CINS.iter().find(|(c, _)| *c == first) { Some((_, region)) if first.is_ascii_alphabetic() => ok("CINS", &s, json!({ "issuer": &s[..6], "issue": &s[6..8], "region": region, "check_digit": &s[8..] })), _ => ok("CUSIP", &s, json!({ "issuer": &s[..6], "issue": &s[6..8], "check_digit": &s[8..] })), } } "SEDOL" => { if s.len() != 7 || s.chars().any(|c| "AEIOU".contains(c)) || !s.chars().all(|c| c.is_ascii_alphanumeric()) { return bad("SEDOL", &s, "a SEDOL is 6 letters (no vowels) or digits and a check digit"); } if sedol_digit(&s) != last_digit(&s) { return bad("SEDOL", &s, typo); } ok("SEDOL", &s, json!({ "check_digit": &s[6..] })) } _ => { if s.len() != 12 || s.as_bytes()[2] != b'G' || s.chars().any(|c| "AEIOU".contains(c)) || !s.chars().all(|c| c.is_ascii_alphanumeric()) { return bad("FIGI", &s, "a FIGI is a 2-letter prefix, G, 8 letters (no vowels) or digits and a check digit"); } if figi_digit(&s) != last_digit(&s) { return bad("FIGI", &s, typo); } ok("FIGI", &s, json!({ "prefix": &s[..2], "check_digit": &s[11..] })) } }}/// Whether `id` is a valid ISIN, CUSIP, SEDOL or FIGI.pub fn security_id_valid(id: &str) -> bool { security_id_check(id, "")["valid"] == json!(true)}/// An ISIN from a national number and its country (ISO code): a CUSIP for "US" or "CA", a SEDOL for "GB" or "IE" (padded with 00),/// any other national number padded with zeros to nine characters (a WKN for "DE", a Valoren for "CH"). The national number's own/// check digit is checked first where it has one.pub fn isin_from(nsin: &str, country: &str) -> Result<String, String> { let n = clean(nsin); let c = country.trim().to_uppercase(); if c.len() != 2 || !c.chars().all(|x| x.is_ascii_alphabetic()) { return Err("the country is a 2-letter code".into()); } let body = match (c.as_str(), n.len()) { ("US" | "CA", 9) => { if cusip_digit(&n) != last_digit(&n) { return Err("the CUSIP's check digit does not match".into()); } n } ("GB" | "IE", 7) => { if sedol_digit(&n) != last_digit(&n) { return Err("the SEDOL's check digit does not match".into()); } format!("00{}", n) } (_, l) if (1..=9).contains(&l) && n.chars().all(|x| x.is_ascii_alphanumeric()) => format!("{:0>9}", n), _ => return Err("a national number has 1 to 9 letters or digits".into()), }; let partial = format!("{}{}", c, body); Ok(format!("{}{}", partial, isin_digit(&partial).ok_or("not letters and digits")?))}/// Every valid ISIN in a text (a fund factsheet, a broker statement), each once in the order found.pub fn isin_find(text: &str) -> Vec<String> { let mut out: Vec<String> = vec![]; for w in text.split(|c: char| !c.is_ascii_alphanumeric()) { if w.len() == 12 && security_id_check(w, "ISIN")["valid"] == json!(true) { let u = w.to_uppercase(); if !out.contains(&u) { out.push(u); } } } out}Related
check_digits.qwer · 1.0.0Check digits made and checked: ISBN-10 and ISBN-13 with conversion, EAN-13, EAN-8 and UPC-A barcodes, IMEI, Luhn, ISO 7064 mod 97-10 references and Verhoeff.6 views · 7 downloadsISBN isbn.qwer · 1.0.0ISBNs checked, converted between 10 and 13 digits, hyphenated by the ISBN Agency's ranges into group, publisher and title parts with the group's language or country named, and every ISBN in a text found.6 views · 6 downloadsLegal Entity Identifiers lei.qwer · 1.0.0Legal Entity Identifiers (ISO 17442) checked by their check digits, taken apart, made from an 18-character base, found in text, and looked up in GLEIF's public register for the company's name and status.8 views · 9 downloadsUS routing numbers aba_routing.qwer · 1.0.0US bank routing numbers checked by their check digit and read for the Federal Reserve district and kind of institution, a check's MICR line read into routing, account and check number, and Canadian transit numbers.7 views · 6 downloadsOFX statements ofx.qwer · 1.0.0Bank and credit card statements in OFX and QFX, old SGML or XML: accounts, balances and each transaction with its bank id, ready to import without doubles.6 views · 8 downloadsCard numbers cards.qwer · 1.0.0Payment card numbers checked before they reach a payment provider: Luhn check digit, brand and length, expiry, and the number formatted or masked for receipts.7 views · 7 downloadsThread 0 posts
On Security identifiers: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.