Validation

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

#finance#validation#codes#import

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) → any

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).

security_id_valid(id: str) → bool

Whether id is a valid ISIN, CUSIP, SEDOL or FIGI.

isin_from(nsin: str, country: str) → stror an error

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.

isin_find(text: str) → list

Every 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/securities_ids.qwer$ curl -O https://quillolang.org/plugins/files/securities_ids.qwe

Then, in your program, all of it or only what you call:

main.qwe
use securities_ids.qweruse security_id_check, security_id_valid from securities_ids      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

securities_ids.qwe
// 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

securities_ids.qwer
//! 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}
Check digits 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.validationbarcodesbooksimport6 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.booksvalidationcodesebooks6 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.financebankingcompliancecodes8 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.bankingpaymentsvalidationfinance7 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.bankingimportaccountingfinance6 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.paymentsformscheckoutvalidation7 views · 7 downloads
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On Security identifiers: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.